diff options
author | Thomas Schmid <thsmi@users.noreply.github.com> | 2021-04-24 13:15:45 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-04-24 13:15:45 +0300 |
commit | 09fac3ff19d63fc6eac67fe930369908da1d9644 (patch) | |
tree | cc62723f6dfc6ee6ce6068bb711e5f44a2de8f1f /src | |
parent | 84f1046b5562fa65db52a1258792e69b2216b934 (diff) |
Validate sha256 fingerprints (#402)
Bad Certs:
* Validate sha256 fingerprints
* Display SHA256 fingerprint and improve message
* Automatic Reconnect when bad cert was accepted.
Security:
* Switch to webcrypto api
* Add SCRAM-SHA512
* Remove CRAM-MD5
* Fix Login with extended ascii character password.
* Allow only secure connection in wx
* Implement a UTF8 base64 en- and decoder.
Backend:
* Move sieve sessions to background page so that only sockets are wx experiments.
* Revise Request and WebExtension's socket implementation.
* Refactor Request Queueing
* Unify Logger a moz specific is not needed anymore.
* Improve log messages.
* Add a trace mode to logger.
* Migrate libManageSieve to JavaScript Modules
* Migrate Gulp scripts to mjs modules
* Rename js files to mjs and tpl to html
* Extract Node specific UI elements into separate files
* Fix Thunderbird 89 compatibility
* Fix loading keytar
* Correct main windows scope
Misc:
* Fix linter and linter warnings
* Adds a Reload and open Devtools button to debug dialog.
* Upgrade to Bootstrap 5b3
* Fix azure configuration
* Upgrade dependencies
Diffstat (limited to 'src')
311 files changed, 40727 insertions, 18214 deletions
diff --git a/src/app/app.html b/src/app/app.html index c591c5ee..a9d87f34 100755 --- a/src/app/app.html +++ b/src/app/app.html @@ -24,7 +24,7 @@ <div id="tabs-scroll-box"> <ul id="tabs-items" class="nav nav-tabs list" role="tablist"> <li id="accounts-tab" class="nav-item"> - <a class="nav-link active" data-toggle="tab" href="#accounts" role="tab">Home</a> + <a class="nav-link active" data-bs-toggle="tab" href="#accounts" role="tab">Home</a> </li> </ul> </div> @@ -43,7 +43,7 @@ ================================================== --> <!-- Placed at the end of the document so the pages load faster --> <script src="./libs/bootstrap/js/bootstrap.bundle.min.js"></script> - <script src="./app.js"></script> + <script type="module" src="./app.mjs"></script> </body> </html>
\ No newline at end of file diff --git a/src/app/app.js b/src/app/app.mjs index e479edeb..2178af18 100755..100644 --- a/src/app/app.js +++ b/src/app/app.mjs @@ -9,35 +9,33 @@ * Thomas Schmid <schmid-thomas@gmx.net> */ -(async function () { - - const DEFAULT_AUTHENTICATION = 0; - const DEFAULT_AUTHORIZATION = 3; +const DEFAULT_AUTHORIZATION = 3; - const FIRST_ELEMENT = 0; +const FIRST_ELEMENT = 0; - const { ipcRenderer } = require('electron'); +const { ipcRenderer } = require('electron'); - // Import the node modules into our global namespace... - const { SieveLogger } = require("./libs/managesieve.ui/utils/SieveLogger.js"); - const { SieveIpcClient} = require("./libs/managesieve.ui/utils/SieveIpcClient.js"); +// Import the node modules into our global namespace... +import { SieveLogger } from "./libs/managesieve.ui/utils/SieveLogger.mjs"; +import { SieveIpcClient } from "./libs/managesieve.ui/utils/SieveIpcClient.mjs"; - const { - SieveCertValidationException - } = require("./libs/libManageSieve/SieveExceptions.js"); +import { + SieveCertValidationException +} from "./libs/libManageSieve/SieveExceptions.mjs"; - const { SieveSessions } = require("./libs/libManageSieve/SieveSessions.js"); +import { SieveSessions } from "./libs/libManageSieve/SieveSessions.mjs"; - const { SieveAccounts } = require("./libs/managesieve.ui/settings/logic/SieveAccounts.js"); +import { SieveAccounts } from "./libs/managesieve.ui/settings/logic/SieveAccounts.mjs"; - const { SieveUpdater } = require("./libs/managesieve.ui/updater/SieveUpdater.js"); - const { SieveTabUI } = require("./libs/managesieve.ui/tabs/SieveTabsUI.js"); +import { SieveUpdater } from "./libs/managesieve.ui/updater/SieveUpdater.mjs"; +import { SieveTabUI } from "./libs/managesieve.ui/tabs/SieveTabsUI.mjs"; - const { SieveThunderbirdImport } = require("./libs/managesieve.ui/importer/SieveThunderbirdImport.js"); - const { SieveAutoConfig } = require("./libs/libManageSieve/SieveAutoConfig.js"); +import { SieveThunderbirdImport } from "./libs/managesieve.ui/importer/SieveThunderbirdImport.mjs"; +import { SieveAutoConfig } from "./libs/libManageSieve/SieveAutoConfig.mjs"; - const { SieveI18n } = require("./libs/managesieve.ui/utils/SieveI18n.js"); +import { SieveI18n } from "./libs/managesieve.ui/utils/SieveI18n.mjs"; +(async function () { const logger = SieveLogger.getInstance(); // TODO remove me this file should not have any dependency to i18n @@ -47,6 +45,13 @@ const accounts = await (new SieveAccounts().load()); const sessions = new SieveSessions(); + let keytar = null; + try { + keytar = require("./libs/keytar"); + } catch (ex) { + logger.log("Could not initialize keytar " + ex); + } + const actions = { "update-check": async () => { @@ -146,7 +151,7 @@ }; }, - "settings-get-loglevel": async function() { + "settings-get-loglevel": async function () { return await accounts.getLogLevel(); }, @@ -167,8 +172,8 @@ const account = accounts.getAccountById(msg.payload.account); return { - "account" : await account.getSettings().getLogLevel(), - "global" : await accounts.getLogLevel() + "account": await account.getSettings().getLogLevel(), + "global": await accounts.getLogLevel() }; }, @@ -184,9 +189,9 @@ sasl: await account.getSecurity().getMechanism() }, "authentication": { - type: await (await account.getAuthentication()).getType(), - username: await (await account.getAuthentication(DEFAULT_AUTHENTICATION)).getUsername(), - stored: await (await account.getAuthentication(DEFAULT_AUTHENTICATION)).hasStoredPassword() + + username: await (await account.getAuthentication()).getUsername(), + stored: await (await account.getAuthentication()).hasStoredPassword() }, "authorization": { @@ -196,11 +201,11 @@ }; }, - "account-settings-forget-credentials": async function(msg) { + "account-settings-forget-credentials": async function (msg) { logger.logAction(`Forget credentials for ${msg.payload.account}`); const account = await accounts.getAccountById(msg.payload.account); - await (await account.getAuthentication(DEFAULT_AUTHENTICATION)).forget(); + await (await account.getAuthentication()).forget(); }, "account-settings-set-credentials": async function (msg) { @@ -212,8 +217,7 @@ await account.getSecurity().setSecure(msg.payload.general.secure); await account.getSecurity().setMechanism(msg.payload.general.sasl); - await account.setAuthentication(msg.payload.authentication.mechanism); - await (await account.getAuthentication(DEFAULT_AUTHENTICATION)).setUsername(msg.payload.authentication.username); + await account.getAuthentication().setUsername(msg.payload.authentication.username); await account.setAuthorization(msg.payload.authorization.mechanism); await (await account.getAuthorization(DEFAULT_AUTHORIZATION)).setAuthorization(msg.payload.authorization.username); @@ -234,7 +238,7 @@ await host.setKeepAlive(msg.payload.keepAlive); }, - "account-import" : async function() { + "account-import": async function () { logger.logAction("Import account settings"); const options = { @@ -261,7 +265,7 @@ await accounts.import(data); }, - "account-export" : async function(msg) { + "account-export": async function (msg) { logger.logAction("Export account settings"); const host = await accounts.getAccountById(msg.payload.account).getHost(); @@ -308,6 +312,11 @@ } catch (e) { + // As first step we disconnect. Our connection sequence failed. + // So ensure the connection is closed. Anyhow we have no chance to recover. + + await (actions["account-disconnect"](response)); + if (e instanceof SieveCertValidationException) { const secInfo = e.getSecurityInfo(); @@ -320,21 +329,24 @@ const host = await accounts.getAccountById(account).getHost(); - await host.setFingerprint(secInfo.fingerprint); + // Prefer SHA256 if available + if ((typeof (secInfo.fingerprint256) !== "undefined") && (secInfo.fingerprint256 !== null)) + await host.setFingerprint(secInfo.fingerprint256); + else + await host.setFingerprint(secInfo.fingerprint); + await host.setIgnoreCertErrors(secInfo.code); - await actions["account-connecting"](response); + await actions["account-connect"](response); return; } // connecting failed for some reason, which means we // need to handle the error. - console.error(e); + logger.logAction("Connecting failed due to an error " + e); await SieveIpcClient.sendMessage( "accounts", "account-show-error", e.message); - - throw e; } }, @@ -559,7 +571,7 @@ return value; }, - "get-default-preference": async(msg) => { + "get-default-preference": async (msg) => { const name = msg.payload.data; logger.logAction(`Get default value for ${name}`); @@ -577,13 +589,37 @@ await accounts.getAccountById(account).getEditor().setValue(name, value); }, - "set-default-preference": async(msg) => { + "set-default-preference": async (msg) => { const name = msg.payload.key; const value = msg.payload.value; logger.logAction(`Set default value for ${name}`); await accounts.getEditor().setValue(name, value); + }, + + "open-developer-tools": async() => { + await ipcRenderer.invoke("open-developer-tools"); + }, + + "reload-ui" : async() => { + await ipcRenderer.invoke("reload-ui"); + }, + + "keystore-ready" : async() => { + return ((typeof(keytar) !== undefined) && (keytar !== null)); + }, + + "keystore-forget" : async(msg) => { + await keytar.deletePassword("Sieve Editor", msg.payload.username); + }, + + "keystore-store" : async(msg) => { + await keytar.setPassword("Sieve Editor", msg.payload.username, msg.payload.password); + }, + + "keystore-get" : async(msg) => { + return await keytar.getPassword("Sieve Editor", msg.payload.username); } }; diff --git a/src/app/css/navbar-top-fixed.css b/src/app/css/navbar-top-fixed.css index ddc05eb7..c69a3077 100755 --- a/src/app/css/navbar-top-fixed.css +++ b/src/app/css/navbar-top-fixed.css @@ -3,10 +3,8 @@ html, body { height: 100%; overflow: hidden; - height: 100%; } - #ctx { display: inline-flex; height: 100%; @@ -14,7 +12,7 @@ body { flex-direction: column; } -#tabs-content>* { +#tabs-content > * { height: 100%; width: 100%; border: 0; @@ -28,36 +26,36 @@ body { #tabs-container { height: 50px; - padding: 0px; + padding: 0; } #tabs-scroll-left { position: absolute; - bottom: 0px; - left: 0px; + bottom: 0; + left: 0; width: 20px; - padding-top: .5rem; - padding-bottom: .5rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; cursor: pointer; } #tabs-scroll-right { position: absolute; - bottom:0px; - right: 0px; + bottom: 0; + right: 0; width: 20px; - padding-top: .5rem; - padding-bottom: .5rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; cursor: pointer; } #tabs-scroll-box { position: absolute; - bottom: 0px; - left:20px; - right:20px; + bottom: 0; + left: 20px; + right: 20px; overflow: hidden; white-space: nowrap; @@ -70,5 +68,5 @@ body { } #tabs-items > li { - display:inline-block; + display: inline-block; } diff --git a/src/app/index.js b/src/app/index.js deleted file mode 100755 index 2fcd0534..00000000 --- a/src/app/index.js +++ /dev/null @@ -1,147 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function () { - - const { app, Menu, BrowserWindow, ipcMain, dialog } = require('electron'); - const path = require('path'); - const url = require('url'); - - const DEFAULT_WINDOW_WIDTH = 1200; - const DEFAULT_WINDOW_HEIGHT = 600; - - // Keep a global reference of the window object, if you don't, the window will - // be closed automatically when the JavaScript object is garbage collected. - let win; - - // ensure we are running as a singleton. - const isLocked = app.requestSingleInstanceLock(); - - if (!isLocked) { - app.quit(); - return; - } - - app.on('second-instance', () => { - // Someone tried to run a second instance, we should focus our window. - if (!win) - return; - - if (win.isMinimized()); - win.restore(); - - win.focus(); - }); - - ipcMain.handle("open-dialog", async(event, options) => { - return await dialog.showOpenDialog(options); - }); - - ipcMain.handle("save-dialog", async(event, options) => { - return await dialog.showSaveDialog(options); - }); - - ipcMain.handle("get-version", async() => { - return await app.getVersion(); - }); - - /** - * Creates the main window - */ - function createWindow() { - - let icon = undefined; - if (process.platform === "linux") - icon = path.join(__dirname, 'libs/icons/linux.png'); - - // Create the browser window. - win = new BrowserWindow({ - width: DEFAULT_WINDOW_WIDTH, - height: DEFAULT_WINDOW_HEIGHT, - icon: icon, - webPreferences: { - // nodeIntegrationInSubFrames: true, - nodeIntegration: true - } - }); - - // Hide the menu bar. - win.removeMenu(); - - // and load the index.html of the app. - win.loadURL(url.format({ - pathname: path.join(__dirname, 'app.html'), - protocol: 'file:', - slashes: true - })); - - // Open the DevTools. - // win.webContents.openDevTools(); - - // Emitted when the window is closed. - win.on('closed', () => { - // Dereference the window object, usually you would store windows - // in an array if your app supports multi windows, this is the time - // when you should delete the corresponding element. - win = null; - }); - - // As suggested in https://github.com/electron/electron/issues/4068 - const inputMenu = Menu.buildFromTemplate([ - { role: 'cut' }, - { role: 'copy' }, - { role: 'paste' } - ]); - - win.webContents.on('context-menu', (e, props) => { - const { isEditable } = props; - if (isEditable) { - inputMenu.popup(win); - } - }); - - const handleRedirect = (e, uri) => { - if (uri !== win.webContents.getURL()) { - e.preventDefault(); - require('electron').shell.openExternal(uri); - } - }; - - // win.webContents.on('will-navigate', handleRedirect); - win.webContents.on('new-window', handleRedirect); - } - - // This method will be called when Electron has finished - // initialization and is ready to create browser windows. - // Some APIs can only be used after this event occurs. - app.on('ready', createWindow); - - // Quit when all windows are closed. - app.on('window-all-closed', () => { - // On macOS it is common for applications and their menu bar - // to stay active until the user quits explicitly with Cmd + Q - if (process.platform !== 'darwin') { - app.quit(); - } - }); - - app.on('activate', () => { - // On macOS it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - if (win === null) { - createWindow(); - } - }); - - // In this file you can include the rest of your app's specific main process - // code. You can also put them in separate files and require them here. - -})(); diff --git a/src/app/libs/libManageSieve/SieveAutoConfig.js b/src/app/libs/libManageSieve/SieveAutoConfig.js deleted file mode 100755 index 2a693ec5..00000000 --- a/src/app/libs/libManageSieve/SieveAutoConfig.js +++ /dev/null @@ -1,111 +0,0 @@ -/* -* The content of this file is licensed. You may obtain a copy of -* the license at https://github.com/thsmi/sieve/ or request it via -* email from the author. -* -* Do not remove or change this comment. -* -* The initial author of the code is: -* Thomas Schmid <schmid-thomas@gmx.net> -*/ - - -(function (exports) { - - "use strict"; - - const SIEVE_PORT_NEW = 4190; - const SIEVE_PORT_OLD = 2000; - - const LOG_LEVEL = 255; - - const { SieveLogger } = require("./SieveLogger.js"); - const { Sieve } = require("./SieveClient.js"); - const { SieveInitRequest } = require("./SieveRequest.js"); - - /** - * Tries to detect the correct sieve port. - * It is typically either 2000 or 4190. - */ - class SieveAutoConfig { - - /** - * Creates a new auto config instance. It tries to automagically detect - * the correct sieve settings. - * - * @param {string} hostname - * the hostname or ip which should be tested - */ - constructor(hostname) { - this.hostname = hostname; - this.logger = new SieveLogger(); - this.logger.level(LOG_LEVEL); - } - - /** - * Auto detects the sieve port for all well known sieve ports. - * In case auto detect fails an exception is thrown. - * - * @returns {int} - * the sieve port in case auto detect succeeds. - */ - async detect() { - - for (const port of [SIEVE_PORT_NEW, SIEVE_PORT_OLD]) - if (await this.probe(port)) - return port; - - throw new Error("Could not detect the sieve port"); - } - - /** - * Tries a handshake on the given port. - * @param {int} port - * the tcp port which should be challenged - * @returns {boolean} - * true in case it is a manage sieve port otherwise false. - */ - async probe(port) { - - const sieve = new Sieve(this.logger); - - return await new Promise((resolve) => { - - const listener = { - - onInitResponse: function () { - resolve(true); - sieve.disconnect(); - }, - - onError: function () { - resolve(false); - sieve.disconnect(); - }, - - onTimeout: function () { - resolve(false); - sieve.disconnect(); - }, - - onDisconnect: function () { - // we are already disconnected.... - resolve(false); - } - }; - - const request = new SieveInitRequest(); - request.addErrorListener(listener.onError); - request.addResponseListener(listener.onInitResponse); - sieve.addRequest(request); - - sieve.addListener(listener); - - sieve.connect(this.hostname, port, false); - }); - } - } - - exports.SieveAutoConfig = SieveAutoConfig; - -})(exports || this); diff --git a/src/app/libs/libManageSieve/SieveAutoConfig.mjs b/src/app/libs/libManageSieve/SieveAutoConfig.mjs new file mode 100644 index 00000000..de647ef9 --- /dev/null +++ b/src/app/libs/libManageSieve/SieveAutoConfig.mjs @@ -0,0 +1,106 @@ +/* +* The content of this file is licensed. You may obtain a copy of +* the license at https://github.com/thsmi/sieve/ or request it via +* email from the author. +* +* Do not remove or change this comment. +* +* The initial author of the code is: +* Thomas Schmid <schmid-thomas@gmx.net> +*/ + + +const SIEVE_PORT_NEW = 4190; +const SIEVE_PORT_OLD = 2000; + +const LOG_LEVEL = 255; + +import { SieveLogger } from "./SieveLogger.mjs"; +import { Sieve } from "./SieveClient.mjs"; +import { SieveInitRequest } from "./SieveRequest.mjs"; + +/** + * Tries to detect the correct sieve port. + * It is typically either 2000 or 4190. + */ +class SieveAutoConfig { + + /** + * Creates a new auto config instance. It tries to automagically detect + * the correct sieve settings. + * + * @param {string} hostname + * the hostname or ip which should be tested + */ + constructor(hostname) { + this.hostname = hostname; + this.logger = new SieveLogger(); + this.logger.level(LOG_LEVEL); + } + + /** + * Auto detects the sieve port for all well known sieve ports. + * In case auto detect fails an exception is thrown. + * + * @returns {int} + * the sieve port in case auto detect succeeds. + */ + async detect() { + + for (const port of [SIEVE_PORT_NEW, SIEVE_PORT_OLD]) + if (await this.probe(port)) + return port; + + throw new Error("Could not detect the sieve port"); + } + + /** + * Tries a handshake on the given port. + * @param {int} port + * the tcp port which should be challenged + * @returns {boolean} + * true in case it is a manage sieve port otherwise false. + */ + async probe(port) { + + const sieve = new Sieve(this.logger); + + // eslint-disable-next-line no-async-promise-executor + return await new Promise(async (resolve) => { + + const listener = { + + onInitResponse: function () { + resolve(true); + sieve.disconnect(); + }, + + onError: function () { + resolve(false); + sieve.disconnect(); + }, + + onTimeout: function () { + resolve(false); + sieve.disconnect(); + }, + + onDisconnected: function () { + // we are already disconnected.... + resolve(false); + } + }; + + const request = new SieveInitRequest(); + request.addErrorListener(listener.onError); + request.addResponseListener(listener.onInitResponse); + await sieve.addRequest(request); + + sieve.addListener(listener); + + sieve.connect(this.hostname, port, false); + }); + } +} + +export { SieveAutoConfig }; diff --git a/src/app/libs/libManageSieve/SieveBase64.mjs b/src/app/libs/libManageSieve/SieveBase64.mjs new file mode 100644 index 00000000..7a275f8c --- /dev/null +++ b/src/app/libs/libManageSieve/SieveBase64.mjs @@ -0,0 +1,55 @@ +/* +* The content of this file is licensed. You may obtain a copy of +* the license at https://github.com/thsmi/sieve/ or request it via +* email from the author. +* +* Do not remove or change this comment. +* +* The initial author of the code is: +* Thomas Schmid <schmid-thomas@gmx.net> +*/ + +import { + SieveAbstractBase64Decoder, + SieveAbstractBase64Encoder +} from "./SieveAbstractBase64.mjs"; + +/** + * + */ +class SieveNodeBase64Encoder extends SieveAbstractBase64Encoder { + + /** + * @inheritdoc + */ + toUtf8() { + return Buffer.from(this.decoded).toString('base64'); + } + + /** + * @inheritdoc + */ + toArray() { + return (new TextEncoder()).encode(this.toUtf8()); + } +} + +/** + * Node implements a native base64 decoder which supports UTF-8 + * This simplifies decoding dramatically. + */ +class SieveNodeBase64Decoder extends SieveAbstractBase64Decoder { + + /** + * @inheritdoc + */ + toArray() { + return new Uint8Array(Buffer.from(this.encoded, 'base64')); + } + +} + +export { + SieveNodeBase64Decoder as SieveBase64Decoder, + SieveNodeBase64Encoder as SieveBase64Encoder +}; diff --git a/src/app/libs/libManageSieve/SieveClient.js b/src/app/libs/libManageSieve/SieveClient.js deleted file mode 100644 index df0140cd..00000000 --- a/src/app/libs/libManageSieve/SieveClient.js +++ /dev/null @@ -1,351 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - - -(function (exports) { - - "use strict"; - - const { SieveAbstractClient } = require("./SieveAbstractClient.js"); - const { SieveNodeResponseParser } = require("./SieveNodeResponseParser.js"); - const { SieveNodeRequestBuilder } = require("./SieveNodeRequestBuilder.js"); - - const { SieveCertValidationException } = require("./SieveExceptions.js"); - - const net = require('net'); - const tls = require('tls'); - const timers = require('timers'); - - const NOT_FOUND = -1; - - /** - * Uses Node networking to realize a sieve client. - */ - class SieveNodeClient extends SieveAbstractClient { - - - /** - * Creates a new instance - * @param {AbstractLogger} logger - * the logger instance to use - **/ - constructor(logger) { - super(); - - this.tlsSocket = null; - this._logger = logger; - this.secure = true; - } - - /** - * @inheritdoc - */ - isSecure() { - return this.secure; - } - - - /** - * @inheritdoc - */ - onStartTimeout() { - - // Clear any existing timeouts - if (this.timeoutTimer) { - timers.clearTimeout(this.timeoutTimer); - this.timeoutTimer = null; - } - - // ensure the idle timer is stopped - this.onStopIdle(); - - // then restart the timeout timer - this.timeoutTimer = timers.setTimeout( - () => { this.onTimeout(); }, - this.getTimeoutWait()); - } - - /** - * @inheritdoc - */ - onStopTimeout() { - - // clear any existing timeout - if (this.timeoutTimer) { - timers.clearTimeout(this.timeoutTimer); - this.timeoutTimer = null; - } - - // and start the idle timer. - this.onStartIdle(); - - return; - } - - /** - * @inheritdoc - */ - onStartIdle() { - // first ensure the timer is stopped.. - this.onStopIdle(); - - // ... then configure the timer. - const delay = this.getIdleWait(); - - if (!delay) - return; - - this.idleTimer - = timers.setTimeout(async () => { await this.onIdle(); }, delay); - } - - /** - * @inheritdoc - */ - onStopIdle() { - - if (!this.idleTimer) - return; - - timers.clearTimeout(this.idleTimer); - this.idleTimer = null; - } - - /** - * @inheritdoc - */ - createParser(data) { - return new SieveNodeResponseParser(data); - } - - /** - * @inheritdoc - */ - createRequestBuilder() { - return new SieveNodeRequestBuilder(); - } - - /** - * @inheritdoc - */ - getLogger() { - return this._logger; - } - - /** - * Connects to a ManageSieve server. - * @param {string} host - * The target hostname or IP address as String - * @param {int} port - * The target port as integer - * @param {boolean} secure - * If true, a secure socket will be created. This allows switching to a secure - * connection. - * - * @returns {SieveAbstractClient} - * a self reference - */ - connect(host, port, secure) { - - if (this.socket !== null) - return this; - - this.host = host; - this.port = port; - this.secure = secure; - - this.socket = net.connect(this.port, this.host); - - this.socket.on('data', (data) => { this.onReceive(data); }); - this.socket.on('error', (error) => { - // Node guarantees that close is called after error. - if ((this.listener) && (this.listener.onError)) - (async () => { await this.listener.onError(error); })(); - }); - this.socket.on('close', () => { - this.disconnect(); - - if ((this.listener) && (this.listener.onDisconnect)) - (async () => { await this.listener.onDisconnect(); })(); - }); - - return this; - } - - /** - * @inheritdoc - */ - async startTLS(options) { - - if (options === undefined || options === null) - options = {}; - - if (options.fingerprints === undefined || options.fingerprints === null || options.fingerprints === "") - options.fingerprints = []; - - if (Array.isArray(options.fingerprints) === false) - options.fingerprints = [options.fingerprints]; - - if (options.ignoreCertErrors === undefined || options.ignoreCertErrors === null || options.ignoreCertErrors === "") - options.ignoreCertErrors = []; - - if (Array.isArray(options.ignoreCertErrors) === false) - options.ignoreCertErrors = [options.ignoreCertErrors]; - - await super.startTLS(); - - 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 - }); - - this.tlsSocket.on('secureConnect', () => { - - const cert = this.tlsSocket.getPeerCertificate(true); - - if (this.tlsSocket.authorized === true) { - - // in case the fingerprint is not pinned we can skip right here. - if (!options.fingerprints.length) { - resolve(); - this.getLogger().logState('Socket upgraded! (Chain of Trust)'); - return; - } - - // so let's check the if the server's fingerprint matches the pinned one. - if (options.fingerprints.indexOf(cert.fingerprint) !== NOT_FOUND) { - resolve(); - this.getLogger().logState('Socket upgraded! (Chain of Trust and pinned fingerprint)'); - return; - } - - const secInfo = { - host: this.host, - port: this.port, - - fingerprint: cert.fingerprint, - fingerprint256 : cert.fingerprint256, - - message: "Server fingerprint does not match pinned fingerprint" - }; - - // If not we need to fail right here... - reject(new SieveCertValidationException(secInfo)); - return; - } - - const error = this.tlsSocket.ssl.verifyError(); - - // dealing with self signed certificates - if (options.ignoreCertErrors.indexOf(error.code) !== NOT_FOUND) { - - // Check if the fingerprint is well known... - if (options.fingerprints.indexOf(cert.fingerprint) !== NOT_FOUND) { - resolve(); - - this.getLogger().logState('Socket upgraded! (Trusted Finger Print)'); - return; - } - } - - const secInfo = { - host: this.host, - port: this.port, - - fingerprint : cert.fingerprint, - fingerprint256 : cert.fingerprint256, - - code : error.code, - message : error.message - }; - - reject(new SieveCertValidationException(secInfo)); - - this.tlsSocket.destroy(); - }); - - this.tlsSocket.on('data', (data) => { this.onReceive(data); }); - }); - } - - /** - * @inheritdoc - */ - disconnect() { - - super.disconnect(); - - this.getLogger().logState("Disconnecting..."); - if (this.socket) { - this.socket.destroy(); - this.socket.unref(); - this.socket = null; - } - - if (this.tlsSocket) { - this.tlsSocket.destroy(); - this.tlsSocket.unref(); - this.tlsSocket = null; - } - - this.idleTimer = null; - this.timeoutTimer = null; - - this.getLogger().logState("Disconnected ..."); - } - - /** - * Called when data was received and is ready to be processed. - * @param {object} buffer - * the received data. - */ - onReceive(buffer) { - - this.getLogger().logState(`onDataRead (${buffer.length})`); - - const data = []; - - for (let i = 0; i < buffer.length; i++) { - data[i] = buffer.readUInt8(i); - } - - super.onReceive(data); - } - - /** - * @inheritdoc - */ - onSend(data) { - - if (this.getLogger().isLevelStream()) { - // Force String to UTF-8... - const output = Array.prototype.slice.call( - new Uint8Array(new TextEncoder("UTF-8").encode(data))); - - this.getLogger().logStream(`Client -> Server [Byte Array]:\n${output}`); - } - - if (this.tlsSocket !== null) { - this.tlsSocket.write(data, "utf8"); - return; - } - - this.socket.write(data, "utf8"); - } - } - - - exports.Sieve = SieveNodeClient; - -})(this); diff --git a/src/app/libs/libManageSieve/SieveClient.mjs b/src/app/libs/libManageSieve/SieveClient.mjs new file mode 100644 index 00000000..5eb7490d --- /dev/null +++ b/src/app/libs/libManageSieve/SieveClient.mjs @@ -0,0 +1,278 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +import { SieveAbstractClient } from "./SieveAbstractClient.mjs"; + +import { SieveCertValidationException } from "./SieveExceptions.mjs"; + +const net = require('net'); +const tls = require('tls'); + +/** + * Uses Node networking to realize a sieve client. + */ +class SieveNodeClient extends SieveAbstractClient { + + + /** + * Creates a new instance + * @param {AbstractLogger} logger + * the logger instance to use + **/ + constructor(logger) { + super(); + + this.tlsSocket = null; + this._logger = logger; + this.secure = true; + } + + /** + * @inheritdoc + */ + isSecure() { + return this.secure; + } + + /** + * @inheritdoc + */ + getLogger() { + return this._logger; + } + + /** + * Connects to a ManageSieve server. + * @param {string} host + * The target hostname or IP address as String + * @param {int} port + * The target port as integer + * @param {boolean} secure + * If true, a secure socket will be created. This allows switching to a secure + * connection. + * + * @returns {SieveAbstractClient} + * a self reference + */ + connect(host, port, secure) { + + if (this.socket !== null) + return this; + + this.host = host; + this.port = port; + this.secure = secure; + + this.socket = net.connect(this.port, this.host); + + this.socket.on('data', async (data) => { await this.onReceive(data); }); + this.socket.on('error', (error) => { + this.getLogger().logState(`SieveClient: OnError (Connection ${this.host}:${this.port})`); + // Node guarantees that close is called after error. + if ((this.listener) && (this.listener.onError)) + (async () => { await this.listener.onError(error); })(); + }); + this.socket.on('close', async () => { + this.getLogger().logState(`SieveClient: OnClose (Connection ${this.host}:${this.port})`); + await this.disconnect(); + }); + + return this; + } + + /** + * @inheritdoc + */ + async startTLS(options) { + + if (options === undefined || options === null) + options = {}; + + if (options.fingerprints === undefined || options.fingerprints === null || options.fingerprints === "") + options.fingerprints = []; + + if (Array.isArray(options.fingerprints) === false) + options.fingerprints = [options.fingerprints]; + + if (options.ignoreCertErrors === undefined || options.ignoreCertErrors === null || options.ignoreCertErrors === "") + options.ignoreCertErrors = []; + + if (Array.isArray(options.ignoreCertErrors) === false) + options.ignoreCertErrors = [options.ignoreCertErrors]; + + await super.startTLS(); + + 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 + }); + + this.tlsSocket.on('secureConnect', () => { + + const cert = this.tlsSocket.getPeerCertificate(true); + + if (this.tlsSocket.authorized === true) { + + // in case the fingerprint is not pinned we can skip right here. + if (!options.fingerprints.length) { + resolve(); + this.getLogger().logState('Socket upgraded! (Chain of Trust)'); + return; + } + + // so let's check the if the server's sha1 fingerprint matches the pinned one. + if (options.fingerprints.includes(cert.fingerprint)) { + resolve(); + this.getLogger().logState('Socket upgraded! (Chain of Trust and pinned SHA1 fingerprint)'); + return; + } + + // then check the sha256 fingerprint. + if (options.fingerprints.includes(cert.fingerprint256)) { + resolve(); + this.getLogger().logState('Socket upgraded! (Chain of Trust and pinned SHA256 fingerprint)'); + return; + } + + const secInfo = { + host: this.host, + port: this.port, + + fingerprint: cert.fingerprint, + fingerprint256: cert.fingerprint256, + + message: "Server fingerprint does not match pinned fingerprint" + }; + + // If not we need to fail right here... + reject(new SieveCertValidationException(secInfo)); + return; + } + + const error = this.tlsSocket.ssl.verifyError(); + + // dealing with self signed certificates + if (options.ignoreCertErrors.includes(error.code)) { + + // Check if the fingerprint is well known... + if (options.fingerprints.includes(cert.fingerprint)) { + resolve(); + + this.getLogger().logState('Socket upgraded! (Trusted SHA1 Finger Print)'); + return; + } + + // Check if the fingerprint is well known... + if (options.fingerprints.includes(cert.fingerprint256)) { + resolve(); + + this.getLogger().logState('Socket upgraded! (Trusted SHA256 Finger Print)'); + return; + } + } + + const secInfo = { + host: this.host, + port: this.port, + + fingerprint: cert.fingerprint, + fingerprint256: cert.fingerprint256, + + code: error.code, + message: error.message + }; + + reject(new SieveCertValidationException(secInfo)); + + this.tlsSocket.destroy(); + }); + + this.tlsSocket.on('data', async (data) => { await this.onReceive(data); }); + }); + } + + /** + * @inheritdoc + */ + async disconnect() { + + this.getLogger().logState(`SieveClient: Disconnecting ${this.host}:${this.port}...`); + + // Just a precaution ensures all timers are stopped. + await super.disconnect(); + + // In case the socket is gone we can skip right here + if (!this.socket) + return; + + if (this.socket) { + this.socket.destroy(); + if (this.socket && this.socket.unref) + this.socket.unref(); + this.socket = null; + } + + if (this.tlsSocket) { + this.tlsSocket.destroy(); + if (this.socket && this.socket.unref) + this.tlsSocket.unref(); + this.tlsSocket = null; + } + + if ((this.listener) && (this.listener.onDisconnected)) + await this.listener.onDisconnected(); + + this.getLogger().logState("SieveClient: ... client disconnected."); + } + + /** + * Called when data was received and is ready to be processed. + * @param {object} buffer + * the received data. + */ + async onReceive(buffer) { + + this.getLogger().logState(`onDataRead (${buffer.length})`); + + const data = []; + + for (let i = 0; i < buffer.length; i++) { + data[i] = buffer.readUInt8(i); + } + + await (super.onReceive(data)); + } + + /** + * @inheritdoc + */ + onSend(data) { + + if (this.getLogger().isLevelStream()) { + // Force String to UTF-8... + const output = Array.prototype.slice.call((new TextEncoder()).encode(data)); + + this.getLogger().logStream(`Client -> Server [Byte Array]:\n${output}`); + } + + if (this.tlsSocket !== null) { + this.tlsSocket.write(data, "utf8"); + return; + } + + this.socket.write(data, "utf8"); + } +} + +export { SieveNodeClient as Sieve }; diff --git a/src/app/libs/libManageSieve/SieveCrypto.js b/src/app/libs/libManageSieve/SieveCrypto.js deleted file mode 100755 index 35af38fc..00000000 --- a/src/app/libs/libManageSieve/SieveCrypto.js +++ /dev/null @@ -1,78 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - const { SieveAbstractCrypto } = require("./SieveAbstractCrypto.js"); - - const crypto = require('crypto'); - - /** - * A Electron specific crypto implementation. - */ - class SieveCrypto extends SieveAbstractCrypto { - - /** - * @inheritdoc - */ - HMAC(key, bytes, output) { - - if (typeof(key) === "undefined" || key === null) - throw new Error("Invalid key"); - - if (Array.isArray(key)) - key = Buffer.from(key); - - if (Array.isArray(bytes)) - bytes = Buffer.from(bytes); - - if (typeof(output) === "undefined" || output === null) - output = "latin1"; - - const rv = crypto - .createHmac(this.name, key) - .update(bytes) - .digest(output); - - if (output === "hex") - return rv; - - return this.strToByteArray(rv); - } - - /** - * @inheritdoc - */ - H(bytes, output) { - - if (typeof(output) === "undefined" || output === null) - output = "latin1"; - - if (Array.isArray(bytes)) - bytes = Buffer.from(bytes); - - const rv = crypto.createHash(this.name) - .update(bytes) - .digest(output); - - if (output === "hex") - return rv; - - return this.strToByteArray(rv); - } - - } - - exports.SieveCrypto = SieveCrypto; - -})(module.exports); diff --git a/src/app/libs/libManageSieve/SieveCrypto.mjs b/src/app/libs/libManageSieve/SieveCrypto.mjs new file mode 100644 index 00000000..147ab0e5 --- /dev/null +++ b/src/app/libs/libManageSieve/SieveCrypto.mjs @@ -0,0 +1,115 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +import { SieveAbstractCrypto } from "./SieveAbstractCrypto.mjs"; + +// The hash names as they are used the web crypto api. +const HASH_SHA1 = "SHA-1"; +const HASH_SHA256 = "SHA-256"; +const HASH_SHA512 = "SHA-512"; + +// Node sadly node uses a slightly different naming scheme. +const NODE_HASH_SHA1 = "SHA1"; +const NODE_HASH_SHA256 = "SHA256"; +const NODE_HASH_SHA512 = "SHA512"; + +/** + * A Electron specific crypto implementation. + * + * @deprecated + * Node implements since version 15.x the web crypto api which makes + * this class obsolete as soon as the new api is marked as mature. + * See https://nodejs.org/api/webcrypto.html for more details. + * + */ +class SieveNodeCrypto extends SieveAbstractCrypto { + + /** + * @inheritdoc + */ + getCryptoHash() { + + if (this.name === HASH_SHA1) + return NODE_HASH_SHA1; + + if (this.name === HASH_SHA256) + return NODE_HASH_SHA256; + + if (this.name === HASH_SHA512) + return NODE_HASH_SHA512; + + throw new Error(`Unknown Hash algorithm ${name}`); + } + + /** + * Loads the crypto module either as classic commonsjs or as ecma module. + * @returns {Crypto} + * the crypto module. + */ + async getCrypto() { + if (typeof(require) !== "undefined") + return require("crypto"); + + return await import("crypto"); + } + + /** + * @inheritdoc + */ + async HMAC(key, bytes, output) { + + if (typeof (key) === "undefined" || key === null) + throw new Error("Invalid key"); + + if (Array.isArray(key)) + key = Buffer.from(key); + + if (Array.isArray(bytes)) + bytes = Buffer.from(bytes); + + if (typeof (output) === "undefined" || output === null) + output = "latin1"; + + const rv = + (await this.getCrypto()).createHmac(this.getCryptoHash(), key) + .update(bytes) + .digest(output); + + if (output === "hex") + return rv; + + return this.strToByteArray(rv); + } + + /** + * @inheritdoc + */ + async H(bytes, output) { + + if (typeof (output) === "undefined" || output === null) + output = "latin1"; + + if (Array.isArray(bytes)) + bytes = Buffer.from(bytes); + + const rv = (await this.getCrypto()).createHash(this.getCryptoHash()) + .update(bytes) + .digest(output); + + if (output === "hex") + return rv; + + return this.strToByteArray(rv); + } + +} + +export { SieveNodeCrypto as SieveCrypto }; diff --git a/src/app/libs/libManageSieve/SieveLogger.js b/src/app/libs/libManageSieve/SieveLogger.js deleted file mode 100644 index d2fb06f4..00000000 --- a/src/app/libs/libManageSieve/SieveLogger.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - const { SieveAbstractLogger } = require("./SieveAbstractLogger.js"); - - /** - * Implements a sieve compatible logger instance for node - */ - class SieveNodeLogger extends SieveAbstractLogger { - - /** - * @inheritdoc - */ - log(message, level) { - - if (!this.isLoggable(level)) - return this; - - // eslint-disable-next-line no-console - console.log(`[${this.getTimestamp()} ${this.prefix()} ] ${message}`); - return this; - } - } - - exports.SieveLogger = SieveNodeLogger; - -})(this); - diff --git a/src/app/libs/libManageSieve/SieveNodeRequestBuilder.js b/src/app/libs/libManageSieve/SieveNodeRequestBuilder.js deleted file mode 100755 index 7fc53659..00000000 --- a/src/app/libs/libManageSieve/SieveNodeRequestBuilder.js +++ /dev/null @@ -1,45 +0,0 @@ -/* - * The contents of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via email - * from the author. Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - const { SieveAbstractRequestBuilder } = require("./SieveAbstractRequestBuilder.js"); - - /** - * Realizes a Request builder which uses native node commands - */ - class SieveNodeRequestBuilder extends SieveAbstractRequestBuilder { - - /** - * @inheritdoc - */ - calculateByteLength(data) { - return Buffer.byteLength(data, 'utf8'); - } - - /** - * @inheritdoc - */ - convertToBase64(decoded) { - return Buffer.from(decoded).toString('base64'); - } - - /** - * @inheritdoc - */ - convertFromBase64(encoded) { - return Buffer.from(encoded, 'base64').toString("latin1"); - } - } - - exports.SieveNodeRequestBuilder = SieveNodeRequestBuilder; - -})(this); diff --git a/src/app/libs/libManageSieve/SieveNodeResponseParser.js b/src/app/libs/libManageSieve/SieveNodeResponseParser.js deleted file mode 100755 index a02b16cd..00000000 --- a/src/app/libs/libManageSieve/SieveNodeResponseParser.js +++ /dev/null @@ -1,46 +0,0 @@ -/* - * The contents of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via email - * from the author. Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - const { SieveAbstractResponseParser } = require("./SieveAbstractResponseParser.js"); - const { StringDecoder } = require('string_decoder'); - - /** - * Implements a node specific response parser - */ - class SieveNodeResponseParser extends SieveAbstractResponseParser { - - /** - * @inheritdoc - **/ - convertToString(byteArray) { - return new StringDecoder('utf8').end(Buffer.from(byteArray)).toString(); - } - - /** - * @inheritdoc - **/ - convertToBase64(decoded) { - return Buffer.from(decoded).toString('base64'); - } - - /** - * @inheritdoc - */ - convertFromBase64(encoded) { - return Buffer.from(encoded, 'base64').toString("latin1"); - } - } - - exports.SieveNodeResponseParser = SieveNodeResponseParser; - -})(this); diff --git a/src/app/libs/libManageSieve/SieveSession.js b/src/app/libs/libManageSieve/SieveSession.js deleted file mode 100644 index 4cbc0035..00000000 --- a/src/app/libs/libManageSieve/SieveSession.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - // Enable Strict Mode - "use strict"; - - const { - SieveAbstractSession - } = require("./SieveAbstractSession.js"); - - /** - * @inheritdoc - */ - class SieveNodeSession extends SieveAbstractSession { - - /** - * @inheritdoc - */ - async startTLS() { - - const options = { - fingerprints : this.getOption("certFingerprints"), - ignoreCertErrors : this.getOption("certIgnoreError") - }; - - await super.startTLS(options); - } - - /** - * The default error handler called upon any unhandled error or exception. - * Called e.g. when the connection to the server was terminated unexpectedly. - * - * The default behaviour is to disconnect. - * - * @param {Error} error - * the error message which causes this exceptional state. - */ - async onError(error) { - - this.getLogger().logSession(`OnError: ${error.message}`); - - await this.disconnect(true); - } - - } - - exports.SieveSession = SieveNodeSession; - -})(exports || this); diff --git a/src/app/libs/libManageSieve/SieveSession.mjs b/src/app/libs/libManageSieve/SieveSession.mjs new file mode 100644 index 00000000..dc371bfc --- /dev/null +++ b/src/app/libs/libManageSieve/SieveSession.mjs @@ -0,0 +1,34 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +import { SieveAbstractSession } from "./SieveAbstractSession.mjs"; + +/** + * @inheritdoc + */ +class SieveNodeSession extends SieveAbstractSession { + + /** + * @inheritdoc + */ + async startTLS() { + + const options = { + fingerprints: this.getOption("certFingerprints"), + ignoreCertErrors: this.getOption("certIgnoreError") + }; + + await super.startTLS(options); + } + +} + +export { SieveNodeSession as SieveSession }; diff --git a/src/app/libs/libManageSieve/SieveSessions.js b/src/app/libs/libManageSieve/SieveSessions.js deleted file mode 100644 index 94ccb4fa..00000000 --- a/src/app/libs/libManageSieve/SieveSessions.js +++ /dev/null @@ -1,168 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - const { SieveSession } = require("./SieveSession.js"); - - /** - * Manages Sieve session. - * - * Sessions are identified by a unique id. - * As the account id unique, it is typically - * used as session id. - */ - class SieveSessions { - - /** - * creates a new instance - */ - constructor() { - this.sessions = new Map(); - } - - /** - * Check if the id it a known session. - * - * @param {string} id - * the session id - * @returns {boolean} - * true in case the id is a known session otherwise false. - */ - has(id) { - return this.sessions.has(id); - } - - /** - * Returns the session with the given id. - * In case the id is unknown an exception is thrown. - * - * @param {string} id - * the session id - * @returns {SieveSession} - * the session or an exception. - */ - get(id) { - if (!this.has(id)) - throw new Error(`Unknown session id ${id}`); - - return this.sessions.get(id); - } - - /** - * Called when an authentication is needed - * - * @param {SieveAccount} account - * the account which should be authenticated. - * @param {boolean} hasPassword - * true if the password is needed otherwise false. - * @returns {object} - * an object the the username and optionally the password. - */ - async onAuthenticate(account, hasPassword) { - - const authentication = {}; - - authentication.username = await (await account.getAuthentication()).getUsername(); - - if (hasPassword) - authentication.password = await (await account.getAuthentication()).getPassword(); - - return authentication; - } - - /** - * Called when an authorization is needed. - * - * @param {SieveAccount} account - * the account which should be authorized. - * @returns {string} - * the user name to be authorized as or an empty string. - */ - async onAuthorize(account) { - return await (await account.getAuthorization()).getAuthorization(); - } - - /** - * Called when a proxy lookup is needed. - * - * @param {SieveAccount} account - * the current account. - * @returns {object} - * the proxy information. - */ - async onProxyLookup(account) { - return await account.getProxy().getProxyInfo(); - } - - - /** - * Creates a new session for the given id. - * In case the session id is in use. It will - * terminate the connection, and recreate a - * new session. - * - * @param {string} id - * the unique session id - * @param {SieveAccount} account - * the account with the session's configuration - */ - async create(id, account) { - - await this.destroy(id); - - const host = await account.getHost(); - const security = await account.getSecurity(); - const settings = await account.getSettings(); - - const options = { - secure : await security.isSecure(), - sasl : await security.getMechanism(), - keepAlive : await host.getKeepAlive(), - logLevel : await settings.getLogLevel(), - certFingerprints : await host.getFingerprint(), - certIgnoreError : await host.getIgnoreCertErrors() - }; - - const session = new SieveSession(id, options); - - // TODO move to app so that it can be shared with the wx implementation. - session.on("authenticate", async (hasPassword) => { return await this.onAuthenticate(account, hasPassword); }); - session.on("authorize", async () => { return await this.onAuthorize(account); }); - session.on("proxy", async (hostname, port) => { return await this.onProxyLookup(account, hostname, port); }); - - this.sessions.set(id, session); - } - - /** - * Destroy the session for the given id. - * If active it will disconnect from the server. - * - * @param {string} id - * the unique session id - */ - async destroy(id) { - if (this.has(id)) - await this.get(id).disconnect(); - - this.sessions.delete(id); - } - - } - - if (module.exports) - module.exports.SieveSessions = SieveSessions; - else - exports.SieveSessions = SieveSessions; - -})(this); diff --git a/src/app/libs/libManageSieve/SieveSessions.mjs b/src/app/libs/libManageSieve/SieveSessions.mjs new file mode 100644 index 00000000..7b9bd5f7 --- /dev/null +++ b/src/app/libs/libManageSieve/SieveSessions.mjs @@ -0,0 +1,146 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +import { SieveSession } from "./SieveSession.mjs"; + +/** + * Manages Sieve session. + * + * Sessions are identified by a unique id. + * As the account id unique, it is typically + * used as session id. + */ +class SieveNodeSessions { + + /** + * creates a new instance + */ + constructor() { + this.sessions = new Map(); + } + + /** + * Check if the id it a known session. + * + * @param {string} id + * the session id + * @returns {boolean} + * true in case the id is a known session otherwise false. + */ + has(id) { + return this.sessions.has(id); + } + + /** + * Returns the session with the given id. + * In case the id is unknown an exception is thrown. + * + * @param {string} id + * the session id + * @returns {SieveSession} + * the session or an exception. + */ + get(id) { + if (!this.has(id)) + throw new Error(`Unknown session id ${id}`); + + return this.sessions.get(id); + } + + /** + * Called when an authentication is needed + * + * @param {SieveAccount} account + * the account which should be authenticated. + * @param {boolean} hasPassword + * true if the password is needed otherwise false. + * @returns {object} + * an object the the username and optionally the password. + */ + async onAuthenticate(account, hasPassword) { + + const authentication = {}; + + authentication.username = await (await account.getAuthentication()).getUsername(); + + if (hasPassword) + authentication.password = await (await account.getAuthentication()).getPassword(); + + return authentication; + } + + /** + * Called when an authorization is needed. + * + * @param {SieveAccount} account + * the account which should be authorized. + * @returns {string} + * the user name to be authorized as or an empty string. + */ + async onAuthorize(account) { + return await (await account.getAuthorization()).getAuthorization(); + } + + + + /** + * Creates a new session for the given id. + * In case the session id is in use. It will + * terminate the connection, and recreate a + * new session. + * + * @param {string} id + * the unique session id + * @param {SieveAccount} account + * the account with the session's configuration + */ + async create(id, account) { + + await this.destroy(id); + + const host = await account.getHost(); + const security = await account.getSecurity(); + const settings = await account.getSettings(); + + const options = { + secure: await security.isSecure(), + sasl: await security.getMechanism(), + keepAlive: await host.getKeepAlive(), + logLevel: await settings.getLogLevel(), + certFingerprints: await host.getFingerprint(), + certIgnoreError: await host.getIgnoreCertErrors() + }; + + const session = new SieveSession(id, options); + + session.on("authenticate", async (hasPassword) => { return await this.onAuthenticate(account, hasPassword); }); + session.on("authorize", async () => { return await this.onAuthorize(account); }); + + this.sessions.set(id, session); + } + + /** + * Destroy the session for the given id. + * If active it will disconnect from the server. + * + * @param {string} id + * the unique session id + */ + async destroy(id) { + if (this.has(id)) + await (this.get(id).disconnect()); + + this.sessions.delete(id); + } + +} + +export { SieveNodeSessions as SieveSessions }; diff --git a/src/app/libs/libManageSieve/SieveTimer.mjs b/src/app/libs/libManageSieve/SieveTimer.mjs new file mode 100644 index 00000000..e68b57b8 --- /dev/null +++ b/src/app/libs/libManageSieve/SieveTimer.mjs @@ -0,0 +1,47 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +import { SieveAbstractTimer } from "./SieveAbstractTimer.mjs"; + +/** + * By default node does not inject a timer into the standard context. + * + * Your need to include it via require. All in all it is almost identical + * with the timer used by a javascript window object. But it lives in a + * different namespace. + */ +class SieveNodeTimer extends SieveAbstractTimer { + + /** + * @inheritdoc + */ + start(callback, ms) { + this.cancel(); + + if (ms === 0) + return; + + this.timer = setTimeout(callback, ms); + } + + /** + * @inheritdoc + */ + cancel() { + if (!this.timer) + return; + + clearTimeout(this.timer); + this.timer = null; + } +} + +export { SieveNodeTimer as SieveTimer }; diff --git a/src/app/libs/managesieve.ui/accounts.html b/src/app/libs/managesieve.ui/accounts.html index fa38ebbf..941a7ef2 100644 --- a/src/app/libs/managesieve.ui/accounts.html +++ b/src/app/libs/managesieve.ui/accounts.html @@ -21,32 +21,7 @@ <!-- Placed at the end of the document so the pages load faster --> <script src="./../bootstrap/js/bootstrap.bundle.min.js"></script> - <script src="./utils/SieveFakeRequire.js"></script> - - <script src="./utils/SieveLogger.js"></script> - <script src="./utils/SieveUniqueId.js"></script> - <script src="./utils/SieveAbstractIpcClient.js"></script> - <script src="./utils/SieveIpcClient.js"></script> - <script src="./utils/SieveI18n.js"></script> - <script src="./utils/SieveTemplate.js"></script> - - <script src="./settings/ui/SieveServerSettingsUI.js"></script> - <script src="./settings/ui/SieveCredentialSettingsUI.js"></script> - <script src="./settings/ui/SieveDebugSettingsUI.js"></script> - - <script src="./accounts/SieveCapabilities.js"></script> - <script src="./accounts/SieveScriptUI.js"></script> - <script src="./accounts/SieveAccountUI.js"></script> - <script src="./accounts/SieveAbstractAccounts.js"></script> - <script src="./accounts/SieveAccounts.js"></script> - <script src="./accounts/SieveAccountCreateUI.js"></script> - - <script src="./dialogs/SieveDialogUI.js"></script> - - <script src="./importer/SieveImportUI.js"></script> - <script src="./updater/SieveUpdaterUI.js"></script> - - <script src="accounts.js"></script> + <script type="module" src="accounts.mjs"></script> </div> </body> diff --git a/src/app/libs/managesieve.ui/accounts.js b/src/app/libs/managesieve.ui/accounts.js deleted file mode 100644 index d1c85dcb..00000000 --- a/src/app/libs/managesieve.ui/accounts.js +++ /dev/null @@ -1,188 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function () { - - "use strict"; - - /* global SieveAccounts */ - /* global SieveUpdaterUI */ - /* global SieveIpcClient */ - /* global SieveLogger */ - /* global SieveI18n */ - - /* global SieveCreateScriptDialog */ - /* global SieveDeleteScriptDialog */ - /* global SieveRenameScriptDialog */ - /* global SieveFingerprintDialog */ - /* global SieveScriptBusyDialog */ - /* global SieveDeleteAccountDialog */ - /* global SieveErrorDialog */ - - /* global SievePasswordDialog */ - /* global SieveAuthorizationDialog */ - - /** - * Shows a prompt which asks the user for the new script name. - * - * @returns {string} - * the script name or an empty string in case the dialog was canceled. - */ - async function onCreateScript() { - return await (new SieveCreateScriptDialog()).show(); - } - - /** - * Shows a prompt which asks the user if the script should be deleted. - * - * @param {string} name - * the script name which should be deleted - * - * @returns {boolean} - * true in case the script shall be deleted otherwise false. - */ - async function onDeleteScript(name) { - return await (new SieveDeleteScriptDialog(name)).show(); - } - - /** - * Shows a prompt which asks the user if the script should be renamed. - * - * @param {string} name - * the name which should be renamed - * - * @returns {string} - * the script name in case the dialog. In case the dialog was - * canceled the original name otherwise the new name. - */ - async function onRenameScript(name) { - return await (new SieveRenameScriptDialog(name)).show(); - } - - /** - * Informs the user that the action can't be performed because - * the script is currently in use. - * - * @param {string} name - * the name of the script which was busy - */ - async function onBusy(name) { - await (new SieveScriptBusyDialog(name)).show(); - } - - /** - * Shows a prompt which asks the user if the given account - * should be removed. - * - * @param {string} name - * the account name - * @returns {boolean} - * true in case the account should be deleted otherwise false. - */ - async function onDeleteAccount(name) { - return await (new SieveDeleteAccountDialog(name)).show(); - } - - /** - * Informs the user about a failed certificate validation. - * - * @param {object} secInfo - * the security information with more details about the validation error. - * - * @returns {boolean} - * true in case the certificate should be overwritten otherwise false. - */ - async function onCertError(secInfo) { - return await (new SieveFingerprintDialog(secInfo)).show(); - } - - /** - * Informs the user about a connection error. - * - * @param {string} message - * the detailed connection error. - */ - async function onError(message) { - await (new SieveErrorDialog(message)).show(); - } - - /** - * Requests the password from the user. - * - * @param {string} username - * the username for which the password is requested. - * @param {string} account - * the account's display name. - * @param {boolean} remember - * show the "remember password" field. - * @returns {string} - * the password as string. - */ - async function onAuthenticate(username, account, remember) { - return await (new SievePasswordDialog(username, account, {remember : remember})).show(); - } - - /** - * Prompts for the username to be authorized. - * - * @param {string} account - * the accounts displayname. - * @returns {string} - * the username to be authorized. - */ - async function onAuthorize(account) { - return await (new SieveAuthorizationDialog(account)).show(); - } - - /** - * The main entry point for the account view - * Called as soon as the DOM is ready. - */ - async function main() { - - SieveLogger.getInstance().level( - await SieveIpcClient.sendMessage("core", "settings-get-loglevel")); - - await (SieveI18n.getInstance()).load(); - - const accounts = new SieveAccounts(); - - SieveIpcClient.setRequestHandler("accounts", "script-show-create", - async () => { return await onCreateScript(); }); - SieveIpcClient.setRequestHandler("accounts", "script-show-delete", - async (msg) => { return await onDeleteScript(msg.payload); }); - SieveIpcClient.setRequestHandler("accounts", "script-show-rename", - async (msg) => { return await onRenameScript(msg.payload); }); - SieveIpcClient.setRequestHandler("accounts", "script-show-busy", - async (msg) => { await onBusy(msg.payload); }); - - SieveIpcClient.setRequestHandler("accounts", "account-show-delete", - async (msg) => { return await onDeleteAccount(msg.payload); }); - SieveIpcClient.setRequestHandler("accounts", "account-show-certerror", - async (msg) => { return await onCertError(msg.payload); }); - SieveIpcClient.setRequestHandler("accounts", "account-show-error", - async (msg) => { return await onError(msg.payload); }); - - SieveIpcClient.setRequestHandler("accounts", "account-show-authentication", - async (msg) => { return await onAuthenticate(msg.payload.username, msg.payload.displayname, msg.payload.remember); }); - SieveIpcClient.setRequestHandler("accounts", "account-show-authorization", - async (msg) => { return await onAuthorize(msg.payload.displayname); }); - - accounts.render(); - (new SieveUpdaterUI()).check(); - } - - if (document.readyState !== 'loading') - main(); - else - document.addEventListener('DOMContentLoaded', () => { main(); }, { once: true }); - -})(); diff --git a/src/app/libs/managesieve.ui/accounts.mjs b/src/app/libs/managesieve.ui/accounts.mjs new file mode 100644 index 00000000..24f29fc5 --- /dev/null +++ b/src/app/libs/managesieve.ui/accounts.mjs @@ -0,0 +1,183 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +import { SieveLogger } from "./utils/SieveLogger.mjs"; +import { SieveIpcClient } from "./utils/SieveIpcClient.mjs"; +import { SieveI18n } from "./utils/SieveI18n.mjs"; + +import { SieveAccounts } from "./accounts/SieveAccounts.mjs"; +import { SieveUpdaterUI } from "./updater/SieveUpdaterUI.mjs"; +import { + SieveCreateScriptDialog, + SieveDeleteScriptDialog, + SieveRenameScriptDialog, + SieveFingerprintDialog, + SieveScriptBusyDialog, + SieveDeleteAccountDialog, + SieveErrorDialog, + SievePasswordDialog, + SieveAuthorizationDialog +} from "./dialogs/SieveDialogUI.mjs"; + +/** + * Shows a prompt which asks the user for the new script name. + * + * @returns {string} + * the script name or an empty string in case the dialog was canceled. + */ +async function onCreateScript() { + return await (new SieveCreateScriptDialog()).show(); +} + +/** + * Shows a prompt which asks the user if the script should be deleted. + * + * @param {string} name + * the script name which should be deleted + * + * @returns {boolean} + * true in case the script shall be deleted otherwise false. + */ +async function onDeleteScript(name) { + return await (new SieveDeleteScriptDialog(name)).show(); +} + +/** + * Shows a prompt which asks the user if the script should be renamed. + * + * @param {string} name + * the name which should be renamed + * + * @returns {string} + * the script name in case the dialog. In case the dialog was + * canceled the original name otherwise the new name. + */ +async function onRenameScript(name) { + return await (new SieveRenameScriptDialog(name)).show(); +} + +/** + * Informs the user that the action can't be performed because + * the script is currently in use. + * + * @param {string} name + * the name of the script which was busy + */ +async function onBusy(name) { + await (new SieveScriptBusyDialog(name)).show(); +} + +/** + * Shows a prompt which asks the user if the given account + * should be removed. + * + * @param {string} name + * the account name + * @returns {boolean} + * true in case the account should be deleted otherwise false. + */ +async function onDeleteAccount(name) { + return await (new SieveDeleteAccountDialog(name)).show(); +} + +/** + * Informs the user about a failed certificate validation. + * + * @param {object} secInfo + * the security information with more details about the validation error. + * + * @returns {boolean} + * true in case the certificate should be overwritten otherwise false. + */ +async function onCertError(secInfo) { + return await (new SieveFingerprintDialog(secInfo)).show(); +} + +/** + * Informs the user about a connection error. + * + * @param {string} message + * the detailed connection error. + */ +async function onError(message) { + await (new SieveErrorDialog(message)).show(); +} + +/** + * Requests the password from the user. + * + * @param {string} username + * the username for which the password is requested. + * @param {string} account + * the account's display name. + * @param {boolean} remember + * show the "remember password" field. + * @returns {string} + * the password as string. + */ +async function onAuthenticate(username, account, remember) { + return await (new SievePasswordDialog(username, account, { remember: remember })).show(); +} + +/** + * Prompts for the username to be authorized. + * + * @param {string} account + * the accounts displayname. + * @returns {string} + * the username to be authorized. + */ +async function onAuthorize(account) { + return await (new SieveAuthorizationDialog(account)).show(); +} + +/** + * The main entry point for the account view + * Called as soon as the DOM is ready. + */ +async function main() { + + SieveLogger.getInstance().level( + await SieveIpcClient.sendMessage("core", "settings-get-loglevel")); + + await (SieveI18n.getInstance()).load(); + + const accounts = new SieveAccounts(); + + SieveIpcClient.setRequestHandler("accounts", "script-show-create", + async () => { return await onCreateScript(); }); + SieveIpcClient.setRequestHandler("accounts", "script-show-delete", + async (msg) => { return await onDeleteScript(msg.payload); }); + SieveIpcClient.setRequestHandler("accounts", "script-show-rename", + async (msg) => { return await onRenameScript(msg.payload); }); + SieveIpcClient.setRequestHandler("accounts", "script-show-busy", + async (msg) => { await onBusy(msg.payload); }); + + SieveIpcClient.setRequestHandler("accounts", "account-show-delete", + async (msg) => { return await onDeleteAccount(msg.payload); }); + SieveIpcClient.setRequestHandler("accounts", "account-show-certerror", + async (msg) => { return await onCertError(msg.payload); }); + SieveIpcClient.setRequestHandler("accounts", "account-show-error", + async (msg) => { return await onError(msg.payload); }); + + SieveIpcClient.setRequestHandler("accounts", "account-show-authentication", + async (msg) => { return await onAuthenticate(msg.payload.username, msg.payload.displayname, msg.payload.remember); }); + SieveIpcClient.setRequestHandler("accounts", "account-show-authorization", + async (msg) => { return await onAuthorize(msg.payload.displayname); }); + + accounts.render(); + (new SieveUpdaterUI()).check(); +} + +if (document.readyState !== 'loading') + main(); +else + document.addEventListener('DOMContentLoaded', () => { main(); }, { once: true }); diff --git a/src/app/libs/managesieve.ui/accounts/SieveAccountCreateUI.js b/src/app/libs/managesieve.ui/accounts/SieveAccountCreateUI.js deleted file mode 100644 index 6f995a23..00000000 --- a/src/app/libs/managesieve.ui/accounts/SieveAccountCreateUI.js +++ /dev/null @@ -1,72 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - /* global bootstrap */ - /* global SieveTemplate */ - /* global SieveIpcClient */ - - /** - * Imports sieve settings from mailers. - */ - class SieveAccountCreateUI { - - /** - * Shows the import account dialog. - * - * @returns {boolean} - * true in case the dialog as accepted otherwise false. - */ - async show() { - - const dialog = await (new SieveTemplate()) - .load("./accounts/account.dialog.create.tpl"); - document.querySelector("#ctx").appendChild(dialog); - - return await new Promise((resolve) => { - - const modal = new bootstrap.Modal(dialog); - - dialog - .querySelector(".sieve-create-account-btn") - .addEventListener("click", async () => { - - const account = { - name: dialog.querySelector(".sieve-create-account-displayname").value, - hostname: dialog.querySelector(".sieve-create-account-hostname").value, - port: dialog.querySelector(".sieve-create-account-port").value, - username: dialog.querySelector(".sieve-create-account-username").value - }; - - // fix me remove modal2 from dom. - await SieveIpcClient.sendMessage("core", "account-create", account); - modal.hide(); - resolve(true); - }); - - modal.show(); - dialog.addEventListener('hidden.bs.modal', () => { - dialog.parentNode.removeChild(dialog); - resolve(false); - }); - }); - } - } - - if (typeof (module) !== "undefined" && module !== null && module.exports) - module.exports = SieveAccountCreateUI; - else - exports.SieveAccountCreateUI = SieveAccountCreateUI; - -})(this); diff --git a/src/app/libs/managesieve.ui/accounts/SieveAccountCreateUI.mjs b/src/app/libs/managesieve.ui/accounts/SieveAccountCreateUI.mjs new file mode 100644 index 00000000..08a1dfbc --- /dev/null +++ b/src/app/libs/managesieve.ui/accounts/SieveAccountCreateUI.mjs @@ -0,0 +1,63 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +/* global bootstrap */ +import { SieveIpcClient } from "./../utils/SieveIpcClient.mjs"; +import { SieveTemplate } from "./../utils/SieveTemplate.mjs"; + +/** + * Imports sieve settings from mailers. + */ +class SieveAccountCreateUI { + + /** + * Shows the import account dialog. + * + * @returns {boolean} + * true in case the dialog as accepted otherwise false. + */ + async show() { + + const dialog = await (new SieveTemplate()) + .load("./accounts/account.dialog.create.html"); + document.querySelector("#ctx").append(dialog); + + return await new Promise((resolve) => { + + const modal = new bootstrap.Modal(dialog); + + dialog + .querySelector(".sieve-create-account-btn") + .addEventListener("click", async () => { + + const account = { + name: dialog.querySelector(".sieve-create-account-displayname").value, + hostname: dialog.querySelector(".sieve-create-account-hostname").value, + port: dialog.querySelector(".sieve-create-account-port").value, + username: dialog.querySelector(".sieve-create-account-username").value + }; + + // fix me remove modal2 from dom. + await SieveIpcClient.sendMessage("core", "account-create", account); + modal.hide(); + resolve(true); + }); + + modal.show(); + dialog.addEventListener('hidden.bs.modal', () => { + dialog.remove(); + resolve(false); + }); + }); + } +} + +export { SieveAccountCreateUI }; diff --git a/src/app/libs/managesieve.ui/accounts/SieveAccountUI.mjs b/src/app/libs/managesieve.ui/accounts/SieveAccountUI.mjs new file mode 100644 index 00000000..5afea0c8 --- /dev/null +++ b/src/app/libs/managesieve.ui/accounts/SieveAccountUI.mjs @@ -0,0 +1,97 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +import { SieveAbstractAccountUI } from "./SieveAbstractAccountUI.mjs"; + +import { SieveCredentialsSettingsUI } from "./../settings/ui/SieveCredentialSettingsUI.mjs"; +import { SieveServerSettingsUI } from "./../settings/ui/SieveServerSettingsUI.mjs"; + +/** + * A UI renderer for a sieve account + */ +class SieveNodeAccountUI extends SieveAbstractAccountUI{ + + /** + * Renders the settings pane + * + */ + async renderSettings() { + + await super.renderSettings(); + + const elm = document.querySelector(`#siv-account-${this.id} .sieve-settings-content`); + + // ... finally connect the listeners. + if (elm.querySelector(".sieve-account-delete-server")) { + elm.querySelector(".sieve-account-delete-server") + .addEventListener("click", () => { this.remove(); }); + } + + if (elm.querySelector(".sieve-account-edit-server")) { + elm.querySelector(".sieve-account-edit-server") + .addEventListener("click", () => { this.showServerSettings(); }); + } + + if (elm.querySelector(".sieve-account-edit-credentials")) { + elm.querySelector(".sieve-account-edit-credentials") + .addEventListener("click", () => { this.showCredentialSettings(); }); + } + + if (elm.querySelector(".sieve-account-export")) { + elm.querySelector(".sieve-account-export") + .addEventListener("click", () => { this.exportSettings(); }); + } + + } + + + + /** + * Asks the user if he is sure to delete the account. + * If yes it triggers expunging the account settings. + * This can not be undone. + */ + async remove() { + await this.accounts.remove(this); + } + + /** + * Shows the server settings dialog. + */ + async showServerSettings() { + + await (new SieveServerSettingsUI(this)).show(); + + this.renderSettings(); + + // Update the account name it may have changed. + document + .querySelector(`#siv-account-${this.id} .siv-account-name`) + .textContent = await this.send("account-get-displayname"); + } + + /** + * Shows the credential settings dialog. + **/ + showCredentialSettings() { + (new SieveCredentialsSettingsUI(this)).show(); + } + + /** + * Exports the account's settings to a file. + */ + async exportSettings() { + await this.send("account-export"); + } + +} + +export { SieveNodeAccountUI as SieveAccountUI }; diff --git a/src/app/libs/managesieve.ui/accounts/SieveAccounts.js b/src/app/libs/managesieve.ui/accounts/SieveAccounts.js deleted file mode 100644 index b73f140e..00000000 --- a/src/app/libs/managesieve.ui/accounts/SieveAccounts.js +++ /dev/null @@ -1,93 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - /* global SieveAbstractAccounts */ - /* global SieveIpcClient */ - /* global SieveTemplate */ - - /* global SieveImportUI */ - /* global SieveAccountCreateUI */ - - /** - * A UI renderer for a list of sieve accounts - **/ - class SieveAppAccounts extends SieveAbstractAccounts { - - /** - * @inheritdoc - */ - async render() { - - if (!document.querySelector(".siv-accounts-items")) { - - document.querySelector(".siv-accounts").appendChild( - (await (new SieveTemplate()).load("./accounts/accounts.tpl"))); - - document.querySelector("#sieve-account-import-file") - .addEventListener("click", async () => { - await SieveIpcClient.sendMessage("core", "account-import"); - this.render(); - }); - - document.querySelector("#sieve-account-import-thunderbird") - .addEventListener("click", async () => { - await (new SieveImportUI()).show(); - this.render(); - }); - - document.querySelector("#sieve-account-create") - .addEventListener("click", async () => { - await (new SieveAccountCreateUI().show()); - this.render(); - }); - } - - super.render(); - } - - /** - * Removes the account including all settings. - * - * @param {SieveAccountUI} account - * the account which should be removed. - */ - async remove(account) { - - const rv = await account.send("account-delete", account.id); - - if (rv) - await this.render(); - } - - /** - * Create a new account, and initializes it with default settings. - * - * @returns {string} - * the accounts unique id. - */ - async create() { - const id = await SieveIpcClient.sendMessage("core", "account-create"); - await this.render(); - - return id; - } - } - - if (typeof (module) !== "undefined" && module && module.exports) - module.exports.SieveAccounts = SieveAppAccounts; - else - exports.SieveAccounts = SieveAppAccounts; - -})(this); diff --git a/src/app/libs/managesieve.ui/accounts/SieveAccounts.mjs b/src/app/libs/managesieve.ui/accounts/SieveAccounts.mjs new file mode 100644 index 00000000..43ec34fa --- /dev/null +++ b/src/app/libs/managesieve.ui/accounts/SieveAccounts.mjs @@ -0,0 +1,84 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +import { SieveAbstractAccounts } from "./SieveAbstractAccounts.mjs"; +import { SieveIpcClient } from "./../utils/SieveIpcClient.mjs"; +import { SieveTemplate } from "./../utils/SieveTemplate.mjs"; + +import { SieveImportUI } from "./../importer/SieveImportUI.mjs"; +import { SieveAccountCreateUI } from "./SieveAccountCreateUI.mjs"; + +/** + * A UI renderer for a list of sieve accounts + **/ +class SieveNodeAccounts extends SieveAbstractAccounts { + + /** + * @inheritdoc + */ + async render() { + + if (!document.querySelector(".siv-accounts-items")) { + + document.querySelector(".siv-accounts").append( + (await (new SieveTemplate()).load("./accounts/accounts.html"))); + + document.querySelector("#sieve-account-import-file") + .addEventListener("click", async () => { + await SieveIpcClient.sendMessage("core", "account-import"); + this.render(); + }); + + document.querySelector("#sieve-account-import-thunderbird") + .addEventListener("click", async () => { + await (new SieveImportUI()).show(); + this.render(); + }); + + document.querySelector("#sieve-account-create") + .addEventListener("click", async () => { + await (new SieveAccountCreateUI().show()); + this.render(); + }); + } + + super.render(); + } + + /** + * Removes the account including all settings. + * + * @param {SieveAccountUI} account + * the account which should be removed. + */ + async remove(account) { + + const rv = await account.send("account-delete", account.id); + + if (rv) + await this.render(); + } + + /** + * Create a new account, and initializes it with default settings. + * + * @returns {string} + * the accounts unique id. + */ + async create() { + const id = await SieveIpcClient.sendMessage("core", "account-create"); + await this.render(); + + return id; + } +} + +export { SieveNodeAccounts as SieveAccounts }; diff --git a/src/app/libs/managesieve.ui/accounts/account.dialog.create.tpl b/src/app/libs/managesieve.ui/accounts/account.dialog.create.html index 81982c22..70677b8d 100644 --- a/src/app/libs/managesieve.ui/accounts/account.dialog.create.tpl +++ b/src/app/libs/managesieve.ui/accounts/account.dialog.create.html @@ -5,7 +5,7 @@ <div class="modal-header"> <h5 class="modal-title" data-i18n="account.create.title"></h5> - <button type="button" class="btn-close" data-dismiss="modal" aria-label="Close"></button> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> diff --git a/src/app/libs/managesieve.ui/accounts/account.settings.tpl b/src/app/libs/managesieve.ui/accounts/account.settings.html index 2ff4ce3c..f3098448 100644 --- a/src/app/libs/managesieve.ui/accounts/account.settings.tpl +++ b/src/app/libs/managesieve.ui/accounts/account.settings.html @@ -4,12 +4,12 @@ <div style="min-width:8em" data-i18n="account.details.server"></div> <span class="sieve-settings-hostname"></span>: <span class="sieve-settings-port"></span> - <span class="sieve-settings-secure ml-1" data-i18n="account.details.secure"></span> + <span class="sieve-settings-secure ms-1" data-i18n="account.details.secure"></span> </div> <div class="sieve-settings-fingerprint-item d-none"> <div class="d-flex flex-row form-label "> <div style="min-width:8em" data-i18n="account.details.fingerprint"></div> - <div class="sieve-settings-fingerprint"></div> + <div class="sieve-settings-fingerprint text-break"></div> </div> </div> <div class="d-flex flex-row form-label "> @@ -23,10 +23,10 @@ <div class="mt-3"> <button type="button" data-i18n="account.details.server.edit" - class="sieve-account-edit-server btn btn-sm btn-outline-secondary mr-1"></button> + class="sieve-account-edit-server btn btn-sm btn-outline-secondary me-1"></button> <button type="button" data-i18n="account.details.credentials.edit" - class="sieve-account-edit-credentials btn btn-sm btn-outline-secondary mr-1"></button> + class="sieve-account-edit-credentials btn btn-sm btn-outline-secondary me-1"></button> <button type="button" data-i18n="account.details.debugging.edit" class="sieve-account-edit-debug btn btn-sm btn-outline-secondary"></button> diff --git a/src/app/libs/managesieve.ui/accounts/accounts.tpl b/src/app/libs/managesieve.ui/accounts/accounts.html index 4e56b81c..4e56b81c 100644 --- a/src/app/libs/managesieve.ui/accounts/accounts.tpl +++ b/src/app/libs/managesieve.ui/accounts/accounts.html diff --git a/src/app/libs/managesieve.ui/importer/SieveImportUI.js b/src/app/libs/managesieve.ui/importer/SieveImportUI.js deleted file mode 100644 index 8bfaeeaa..00000000 --- a/src/app/libs/managesieve.ui/importer/SieveImportUI.js +++ /dev/null @@ -1,94 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - /* global SieveTemplate */ - /* global SieveIpcClient */ - /* global bootstrap */ - - /** - * Imports sieve settings from mailers. - */ - class SieveImportUI { - - /** - * Shows the import account dialog. - */ - async show() { - - const dialog = await (new SieveTemplate()).load("./importer/account.import.tpl"); - dialog.querySelector(".sieve-import-progress").classList.add("d-none"); - document.querySelector("#ctx").appendChild(dialog); - - // we need to call it on the main thread because we don't have - // to all the libraries we need right here. - const accounts = await SieveIpcClient.sendMessage("core", "import-thunderbird"); - - const modal = new bootstrap.Modal(dialog); - - await new Promise((resolve) => { - - accounts.forEach(async (account) => { - const item = await (new SieveTemplate()).load("./importer/account.import.item.tpl"); - - item.querySelector(".sieve-import-username").textContent = account["username"]; - item.querySelector(".sieve-import-hostname").textContent = account["hostname"]; - item.querySelector(".sieve-import-name").textContent = account["name"]; - - item.querySelector(".sieve-import-source").textContent = "Thunderbird"; - - item.querySelector(".sieve-import-btn").addEventListener("click", async () => { - - dialog.querySelector(".sieve-import-items").classList.add("d-none"); - dialog.querySelector(".sieve-import-progress").classList.remove("d-none"); - - let account2; - try { - account2 = await SieveIpcClient.sendMessage("core", "account-probe", account); - } catch (ex) { - alert(`Failed to import ${ex}`); - resolve(false); - - dialog.querySelector(".sieve-import-items").classList.remove("d-none"); - dialog.querySelector(".sieve-import-progress").classList.add("d-none"); - return; - } - - // fix me remove modal2 from dom. - await SieveIpcClient.sendMessage("core", "account-create", account2); - modal.hide(); - resolve(true); - }); - - dialog.querySelector(".sieve-import-items").appendChild(item); - - }); - - modal.show(); - - dialog.addEventListener('hidden.bs.modal', () => { - dialog.parentNode.removeChild(dialog); - resolve(false); - }); - }); - } - } - - - if (typeof (module) !== "undefined" && module !== null && module.exports) - module.exports = SieveImportUI; - else - exports.SieveImportUI = SieveImportUI; - -})(this); diff --git a/src/app/libs/managesieve.ui/importer/SieveImportUI.mjs b/src/app/libs/managesieve.ui/importer/SieveImportUI.mjs new file mode 100644 index 00000000..d0a1b2aa --- /dev/null +++ b/src/app/libs/managesieve.ui/importer/SieveImportUI.mjs @@ -0,0 +1,84 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +/* global bootstrap */ +import { SieveTemplate } from "./../utils/SieveTemplate.mjs"; +import { SieveIpcClient } from "./../utils/SieveIpcClient.mjs"; + +/** + * Imports sieve settings from mailers. + */ +class SieveImportUI { + + /** + * Shows the import account dialog. + */ + async show() { + + const dialog = await (new SieveTemplate()).load("./importer/account.import.html"); + dialog.querySelector(".sieve-import-progress").classList.add("d-none"); + document.querySelector("#ctx").append(dialog); + + // we need to call it on the main thread because we don't have + // to all the libraries we need right here. + const accounts = await SieveIpcClient.sendMessage("core", "import-thunderbird"); + + const modal = new bootstrap.Modal(dialog); + + await new Promise((resolve) => { + + accounts.forEach(async (account) => { + const item = await (new SieveTemplate()).load("./importer/account.import.item.html"); + + item.querySelector(".sieve-import-username").textContent = account["username"]; + item.querySelector(".sieve-import-hostname").textContent = account["hostname"]; + item.querySelector(".sieve-import-name").textContent = account["name"]; + + item.querySelector(".sieve-import-source").textContent = "Thunderbird"; + + item.querySelector(".sieve-import-btn").addEventListener("click", async () => { + + dialog.querySelector(".sieve-import-items").classList.add("d-none"); + dialog.querySelector(".sieve-import-progress").classList.remove("d-none"); + + let account2; + try { + account2 = await SieveIpcClient.sendMessage("core", "account-probe", account); + } catch (ex) { + alert(`Failed to import ${ex}`); + resolve(false); + + dialog.querySelector(".sieve-import-items").classList.remove("d-none"); + dialog.querySelector(".sieve-import-progress").classList.add("d-none"); + return; + } + + // fix me remove modal2 from dom. + await SieveIpcClient.sendMessage("core", "account-create", account2); + modal.hide(); + resolve(true); + }); + + dialog.querySelector(".sieve-import-items").append(item); + + }); + + modal.show(); + + dialog.addEventListener('hidden.bs.modal', () => { + dialog.remove(); + resolve(false); + }); + }); + } +} + +export { SieveImportUI }; diff --git a/src/app/libs/managesieve.ui/importer/SieveThunderbirdImport.js b/src/app/libs/managesieve.ui/importer/SieveThunderbirdImport.js deleted file mode 100644 index 5a367bda..00000000 --- a/src/app/libs/managesieve.ui/importer/SieveThunderbirdImport.js +++ /dev/null @@ -1,221 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - const PREF_KEY_ACCOUNTS = '^.*user_pref\\(.*"mail.accountmanager.accounts".*,.*"(.*)"\\);.*$'; - const PREF_KEY_SERVER = '^.*user_pref\\(.*"mail.account.%account%.server".*,.*"(.*)"\\);.*$'; - const PREF_KEY_SERVER_TYPE = '^.*user_pref\\(.*"mail.server.%server%.type".*,.*"(.*)"\\);.*$'; - const PREF_KEY_SERVER_USERNAME = '^.*user_pref\\(.*"mail.server.%server%.userName".*,.*"(.*)"\\);.*$'; - const PREF_KEY_SERVER_HOSTNAME = '^.*user_pref\\(.*"mail.server.%server%.hostname".*,.*"(.*)"\\);.*$'; - const PREF_KEY_SERVER_REALUSERNAME = '^.*user_pref\\(.*"mail.server.%server%.realuserName".*,.*"(.*)"\\);.*$'; - const PREF_KEY_SERVER_REALHOSTNAME = '^.*user_pref\\(.*"mail.server.%server%.realhostname".*,.*"(.*)"\\);.*$'; - const PREF_KEY_SERVER_NAME = '^.*user_pref\\(.*"mail.server.%server%.name".*,.*"(.*)"\\);.*$'; - - const FIRST_MATCH = 1; - - /** - * Imports Account settings from thunderbird's profile directory. - */ - class SieveThunderbirdImport { - - /** - * Parses a section from thunderbird's profile.ini - * @param {string} section - * the section which should be parsed. - * @returns {Struct} - * an object containing the path as well as the information - * if the profile is default and the path is relative. - */ - parseProfileSection(section) { - const lines = section.split(/\r?\n/g); - - let path = ""; - let isRelative = false; - let isDefault = false; - - for (let line of lines) { - line = line.trim(); - - if (line.toLocaleLowerCase() === "default=1") - isDefault = true; - - if (line.toLocaleLowerCase().startsWith("path=")) - path = line.split("=")[FIRST_MATCH]; - - if (line.toLocaleLowerCase() === "isrelative=1") - isRelative = true; - } - - return { - "path": path, - "isRelative": isRelative, - "isDefault": isDefault - }; - } - - /** - * Parses Thunderbird's profile.ini and returns the path to the profile. - * @param {string} [directory] - * the directory to thunderbird's app data directory. - * if omitted the directory will be guessed. - * @returns {string} - * the path to the default user Profile. - */ - getDefaultUserProfile(directory) { - const path = require('path'); - const fs = require('fs'); - - if (typeof (directory) === "undefined" || directory === null) - directory = this.getProfileDirectory(); - - const file = fs.readFileSync( - path.join(directory, "profiles.ini"), "utf-8"); - - const sections = file.split(/\[\w*\]/gm); - - for (let section of sections) { - - section = this.parseProfileSection(section); - - if (!section.isDefault) - continue; - - if (section.isRelative) - return path.join(directory, section.path); - - return section.path; - } - - throw new Error("Failed to parse profile.ini"); - - } - - /** - * Tries to get thunderbird's profile directory. - * - * @returns {string} - * the profile directory - */ - getProfileDirectory() { - const path = require('path'); - const fs = require('fs'); - - let directory; - - if (process.platform === "linux") - directory = path.join(process.env.HOME, ".thunderbird"); - else if (process.platform === "win32") - directory = path.join(process.env.APPDATA, "Thunderbird"); - else - throw new Error("Unsupported Platform"); - - if (!fs.existsSync(directory)) - throw new Error("No file path"); - - return directory; - } - - /** - * Tries to read thunderbird's preference file. - * - * @returns {string} - * the current user's preference.js - */ - getProfile() { - const path = require('path'); - const fs = require('fs'); - - const profile = this.getProfileDirectory(); - - this.getDefaultUserProfile(profile); - - return fs.readFileSync( - path.join(this.getDefaultUserProfile(profile), "prefs.js"), "utf-8"); - } - - /** - * Extracts the given key from the server settings. - * - * @param {string} profile - * the profile data - * @param {string} server - * the server's unique name - * @param {string} key - * the preference key to be retrieved - * - * @returns {string} - * the key's value or null in case it does not exist. - */ - getServerKey(profile, server, key) { - const value = (new RegExp(key.replace("%server%", server), "gm")).exec(profile); - - if (!value) - return null; - - return value[FIRST_MATCH]; - } - - /** - * Reads the accounts from thunderbird's preferences file. - * - * @returns {object} - * username and the host for each imap account. - */ - getAccounts() { - - const results = []; - const profile = this.getProfile(); - - const accounts = (new RegExp(PREF_KEY_ACCOUNTS, "gm")).exec(profile)[FIRST_MATCH].split(","); - - for (const account of accounts) { - - const server = (new RegExp(PREF_KEY_SERVER.replace("%account%", account), "gm")).exec(profile)[FIRST_MATCH]; - const type = this.getServerKey(profile, server, PREF_KEY_SERVER_TYPE); - - if (type !== "imap") - continue; - - let username = this.getServerKey(profile, server, PREF_KEY_SERVER_REALUSERNAME); - - if (!username) - username = this.getServerKey(profile, server, PREF_KEY_SERVER_USERNAME); - - let hostname = this.getServerKey(profile, server, PREF_KEY_SERVER_REALHOSTNAME); - if (!hostname) - hostname = this.getServerKey(profile, server, PREF_KEY_SERVER_HOSTNAME); - - const name = this.getServerKey(profile, server, PREF_KEY_SERVER_NAME); - - const result = {}; - - result["username"] = username; - result["hostname"] = hostname; - result["name"] = name; - - results.push(result); - } - - return results; - } - - } - - - if (typeof (module) !== "undefined" && module && module.exports) - module.exports.SieveThunderbirdImport = SieveThunderbirdImport; - else - exports.SieveThunderbirdImport = SieveThunderbirdImport; - -})(this); diff --git a/src/app/libs/managesieve.ui/importer/SieveThunderbirdImport.mjs b/src/app/libs/managesieve.ui/importer/SieveThunderbirdImport.mjs new file mode 100644 index 00000000..84798411 --- /dev/null +++ b/src/app/libs/managesieve.ui/importer/SieveThunderbirdImport.mjs @@ -0,0 +1,208 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +const PREF_KEY_ACCOUNTS = '^.*user_pref\\(.*"mail.accountmanager.accounts".*,.*"(.*)"\\);.*$'; +const PREF_KEY_SERVER = '^.*user_pref\\(.*"mail.account.%account%.server".*,.*"(.*)"\\);.*$'; +const PREF_KEY_SERVER_TYPE = '^.*user_pref\\(.*"mail.server.%server%.type".*,.*"(.*)"\\);.*$'; +const PREF_KEY_SERVER_USERNAME = '^.*user_pref\\(.*"mail.server.%server%.userName".*,.*"(.*)"\\);.*$'; +const PREF_KEY_SERVER_HOSTNAME = '^.*user_pref\\(.*"mail.server.%server%.hostname".*,.*"(.*)"\\);.*$'; +const PREF_KEY_SERVER_REALUSERNAME = '^.*user_pref\\(.*"mail.server.%server%.realuserName".*,.*"(.*)"\\);.*$'; +const PREF_KEY_SERVER_REALHOSTNAME = '^.*user_pref\\(.*"mail.server.%server%.realhostname".*,.*"(.*)"\\);.*$'; +const PREF_KEY_SERVER_NAME = '^.*user_pref\\(.*"mail.server.%server%.name".*,.*"(.*)"\\);.*$'; + +const FIRST_MATCH = 1; + +const path = require('path'); +const fs = require('fs'); + +/** + * Imports Account settings from thunderbird's profile directory. + */ +class SieveThunderbirdImport { + + /** + * Parses a section from thunderbird's profile.ini + * @param {string} section + * the section which should be parsed. + * @returns {Struct} + * an object containing the path as well as the information + * if the profile is default and the path is relative. + */ + parseProfileSection(section) { + const lines = section.split(/\r?\n/g); + + let profileDir = ""; + let isRelative = false; + let isDefault = false; + + for (let line of lines) { + line = line.trim(); + + if (line.toLocaleLowerCase() === "default=1") + isDefault = true; + + if (line.toLocaleLowerCase().startsWith("path=")) + profileDir = line.split("=")[FIRST_MATCH]; + + if (line.toLocaleLowerCase() === "isrelative=1") + isRelative = true; + } + + return { + "path": profileDir, + "isRelative": isRelative, + "isDefault": isDefault + }; + } + + /** + * Parses Thunderbird's profile.ini and returns the path to the profile. + * @param {string} [directory] + * the directory to thunderbird's app data directory. + * if omitted the directory will be guessed. + * @returns {string} + * the path to the default user Profile. + */ + getDefaultUserProfile(directory) { + + if (typeof (directory) === "undefined" || directory === null) + directory = this.getProfileDirectory(); + + const file = fs.readFileSync( + path.join(directory, "profiles.ini"), "utf-8"); + + const sections = file.split(/\[\w*]/gm); + + for (let section of sections) { + + section = this.parseProfileSection(section); + + if (!section.isDefault) + continue; + + if (section.isRelative) + return path.join(directory, section.path); + + return section.path; + } + + throw new Error("Failed to parse profile.ini"); + + } + + /** + * Tries to get thunderbird's profile directory. + * + * @returns {string} + * the profile directory + */ + getProfileDirectory() { + + let directory; + + if (process.platform === "linux") + directory = path.join(process.env.HOME, ".thunderbird"); + else if (process.platform === "win32") + directory = path.join(process.env.APPDATA, "Thunderbird"); + else + throw new Error("Unsupported Platform"); + + if (!fs.existsSync(directory)) + throw new Error("No file path"); + + return directory; + } + + /** + * Tries to read thunderbird's preference file. + * + * @returns {string} + * the current user's preference.js + */ + getProfile() { + + const profile = this.getProfileDirectory(); + + this.getDefaultUserProfile(profile); + + return fs.readFileSync( + path.join(this.getDefaultUserProfile(profile), "prefs.js"), "utf-8"); + } + + /** + * Extracts the given key from the server settings. + * + * @param {string} profile + * the profile data + * @param {string} server + * the server's unique name + * @param {string} key + * the preference key to be retrieved + * + * @returns {string} + * the key's value or null in case it does not exist. + */ + getServerKey(profile, server, key) { + const value = (new RegExp(key.replace("%server%", server), "gm")).exec(profile); + + if (!value) + return null; + + return value[FIRST_MATCH]; + } + + /** + * Reads the accounts from thunderbird's preferences file. + * + * @returns {object} + * username and the host for each imap account. + */ + getAccounts() { + + const results = []; + const profile = this.getProfile(); + + const accounts = (new RegExp(PREF_KEY_ACCOUNTS, "gm")).exec(profile)[FIRST_MATCH].split(","); + + for (const account of accounts) { + + const server = (new RegExp(PREF_KEY_SERVER.replace("%account%", account), "gm")).exec(profile)[FIRST_MATCH]; + const type = this.getServerKey(profile, server, PREF_KEY_SERVER_TYPE); + + if (type !== "imap") + continue; + + let username = this.getServerKey(profile, server, PREF_KEY_SERVER_REALUSERNAME); + + if (!username) + username = this.getServerKey(profile, server, PREF_KEY_SERVER_USERNAME); + + let hostname = this.getServerKey(profile, server, PREF_KEY_SERVER_REALHOSTNAME); + if (!hostname) + hostname = this.getServerKey(profile, server, PREF_KEY_SERVER_HOSTNAME); + + const name = this.getServerKey(profile, server, PREF_KEY_SERVER_NAME); + + const result = {}; + + result["username"] = username; + result["hostname"] = hostname; + result["name"] = name; + + results.push(result); + } + + return results; + } + +} + +export { SieveThunderbirdImport }; diff --git a/src/app/libs/managesieve.ui/importer/account.import.tpl b/src/app/libs/managesieve.ui/importer/account.import.html index b789a59c..4712cbdd 100644 --- a/src/app/libs/managesieve.ui/importer/account.import.tpl +++ b/src/app/libs/managesieve.ui/importer/account.import.html @@ -5,7 +5,7 @@ <div class="modal-header"> <h5 class="modal-title" data-i18n="import.title"></h5> - <button type="button" class="btn-close" data-dismiss="modal" aria-label="Close"></button> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> diff --git a/src/app/libs/managesieve.ui/importer/account.import.item.tpl b/src/app/libs/managesieve.ui/importer/account.import.item.html index 1231c22a..b652f171 100644 --- a/src/app/libs/managesieve.ui/importer/account.import.item.tpl +++ b/src/app/libs/managesieve.ui/importer/account.import.item.html @@ -12,7 +12,7 @@ <label class="col-sm-3 col-form-label" data-i18n="import.username"></label> <div class="sieve-import-username col-sm-8 col-form-label"></div> </div> - <div class="text-right"> + <div class="text-end"> <button type="button" class="sieve-import-btn btn btn-primary" data-i18n="import.accept"></button> </div> </div> diff --git a/src/app/libs/managesieve.ui/settings/logic/SieveAccount.js b/src/app/libs/managesieve.ui/settings/logic/SieveAccount.js deleted file mode 100644 index 8327b6b9..00000000 --- a/src/app/libs/managesieve.ui/settings/logic/SieveAccount.js +++ /dev/null @@ -1,45 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - const { SieveAbstractAccount } = require("./SieveAbstractAccount.js"); - - // const SievePasswordManager = require('./utils/SievePasswordManager.js'); - - /** - * Manages the account specific settings - */ - class SieveAccount extends SieveAbstractAccount { - - /** - * - */ - getProxy() { - return { - getProxyInfo: function () { - return null; - } - }; - } - - - } - - // Require modules need to use export.module - if (typeof (module) !== "undefined" && module && module.exports) - module.exports.SieveAccount = SieveAccount; - else - exports.SieveAccount = SieveAccount; - -})(this); diff --git a/src/app/libs/managesieve.ui/settings/logic/SieveAccount.mjs b/src/app/libs/managesieve.ui/settings/logic/SieveAccount.mjs new file mode 100644 index 00000000..8d2d0012 --- /dev/null +++ b/src/app/libs/managesieve.ui/settings/logic/SieveAccount.mjs @@ -0,0 +1,21 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + + +import { SieveAbstractAccount } from "./SieveAbstractAccount.mjs"; + +/** + * Manages the account specific settings + */ +class SieveAccount extends SieveAbstractAccount { +} + +export { SieveAccount }; diff --git a/src/app/libs/managesieve.ui/settings/logic/SieveAccounts.js b/src/app/libs/managesieve.ui/settings/logic/SieveAccounts.js deleted file mode 100644 index 51128584..00000000 --- a/src/app/libs/managesieve.ui/settings/logic/SieveAccounts.js +++ /dev/null @@ -1,191 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - - -(function (exports) { - - "use strict"; - - const JSON_INDENTATION = 2; - - const CONFIG_ID_GLOBAL = "global"; - const CONFIG_KEY_ACCOUNTS = "accounts"; - - const DEFAULT_AUTHENTICATION = 1; - - const SETTINGS_VERSION_I = 1; - - const { SieveLogger } = require("./../../utils/SieveLogger.js"); - - const { SievePrefManager } = require('./SievePrefManager.js'); - - const { SieveAccount } = require("./SieveAccount.js"); - const { SieveAbstractAccounts } = require("./SieveAbstractAccounts.js"); - - /** - * Manages the configuration for sieve accounts. - * It behaves like a directory. Ist just lists the accounts. - * The individual settings are managed by the SieveAccount object - * - * It uses the DOM's local store to persist the configuration data. - */ - class SieveAccounts extends SieveAbstractAccounts { - - /** - * @inheritdoc - */ - async load() { - - const items = await (new SievePrefManager(CONFIG_ID_GLOBAL)).getComplexValue(CONFIG_KEY_ACCOUNTS, []); - - const accounts = {}; - - SieveLogger.getInstance().level(await this.getLogLevel()); - - if (!items) - return this; - - for (const item of items) { - // Recreate the accounts only when needed... - if (this.accounts[item]) - accounts[item] = this.accounts[item]; - else - accounts[item] = new SieveAccount(item); - } - - this.accounts = accounts; - return this; - } - - /** - * Saves the list of account configurations. - * - * @returns {SieveAccounts} - * a self reference. - */ - async save() { - await (new SievePrefManager(CONFIG_ID_GLOBAL)).setComplexValue(CONFIG_KEY_ACCOUNTS, [...Object.keys(this.accounts)]); - return this; - } - - /** - * Creates a new account. - * The new account will be initialized with default and then added to the list of accounts - * - * @param {object} [details] - * the accounts details like the name, hostname, port and username as key value pairs. - * - * @returns {SieveAccounts} - * a self reference. - */ - async create(details) { - - // create a unique id; - - const id = this.generateId(); - - this.accounts[id] = new SieveAccount(id); - - await this.save(); - - if (typeof (details) === "undefined" || details === null) - return this; - - if ((details.hostname !== null) && (details.hostname !== undefined)) - await (await this.accounts[id].getHost()).setHostname(details.hostname); - - if ((details.port !== null) && (details.port !== undefined)) - await (await this.accounts[id].getHost()).setPort(details.port); - - if ((details.username !== null) && (details.username !== undefined)) - await (await this.accounts[id].getAuthentication(DEFAULT_AUTHENTICATION)).setUsername(details.username); - - if ((details.name !== null) && (details.name !== undefined)) - await (await this.accounts[id].getHost()).setDisplayName(details.name); - - return this; - } - - /** - * Removes the account including all settings. - * - * @param {AccountId} id - * the unique id which identifies the account. - * @returns {SieveAccounts} - * a self reference - */ - async remove(id) { - // remove the accounts... - delete this.accounts[id]; - // ... an persist it. - await this.save(); - - // remove the account's settings. - (new SievePrefManager(`@${id}`)).clear(); - - return this; - } - - /** - * Imports previously exported account settings. - * - * @param {string} data - * the settings to be imported. - */ - async import(data) { - data = JSON.parse(data); - - if (data.version !== SETTINGS_VERSION_I) - throw new Error(`Unknown version ${data.version}`); - - const details = { - name: data.settings["host.displayName"], - hostname: data.settings["hostname"], - port: data.settings["port"], - username: data.settings["authentication.username"] - }; - - await this.create(details); - } - - /** - * Exports the account's settings. - * - * @param {string} id - * the unique account id. - * - * @returns {string} - * the account settings as json string. - */ - async export(id) { - const config = new SievePrefManager(`@${id}`); - - let data = {}; - for (const key of config.getKeys()) - data[key] = await (config.getValue(key)); - - data = { - "version": SETTINGS_VERSION_I, - "settings": data - }; - - return JSON.stringify( - data, null, JSON_INDENTATION); - } - } - - // Require modules need to use export.module - if (module.exports) - module.exports.SieveAccounts = SieveAccounts; - else - exports.SieveAccounts = SieveAccounts; - -})(this); diff --git a/src/app/libs/managesieve.ui/settings/logic/SieveAccounts.mjs b/src/app/libs/managesieve.ui/settings/logic/SieveAccounts.mjs new file mode 100644 index 00000000..789c7984 --- /dev/null +++ b/src/app/libs/managesieve.ui/settings/logic/SieveAccounts.mjs @@ -0,0 +1,180 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + + +const JSON_INDENTATION = 2; + +const CONFIG_ID_GLOBAL = "global"; +const CONFIG_KEY_ACCOUNTS = "accounts"; + + +const SETTINGS_VERSION_I = 1; + +import { SieveLogger } from "./../../utils/SieveLogger.mjs"; + +import { SievePrefManager } from "./SievePrefManager.mjs"; + +import { SieveAccount } from "./SieveAccount.mjs"; +import { SieveAbstractAccounts } from "./SieveAbstractAccounts.mjs"; + +/** + * Manages the configuration for sieve accounts. + * It behaves like a directory. Ist just lists the accounts. + * The individual settings are managed by the SieveAccount object + * + * It uses the DOM's local store to persist the configuration data. + */ +class SieveAccounts extends SieveAbstractAccounts { + + /** + * @inheritdoc + */ + async load() { + + const items = await (new SievePrefManager(CONFIG_ID_GLOBAL)).getComplexValue(CONFIG_KEY_ACCOUNTS, []); + + const accounts = {}; + + SieveLogger.getInstance().level(await this.getLogLevel()); + + if (!items) + return this; + + for (const item of items) { + // Recreate the accounts only when needed... + if (this.accounts[item]) + accounts[item] = this.accounts[item]; + else + accounts[item] = new SieveAccount(item); + } + + this.accounts = accounts; + return this; + } + + /** + * Saves the list of account configurations. + * + * @returns {SieveAccounts} + * a self reference. + */ + async save() { + await (new SievePrefManager(CONFIG_ID_GLOBAL)).setComplexValue(CONFIG_KEY_ACCOUNTS, [...Object.keys(this.accounts)]); + return this; + } + + /** + * Creates a new account. + * The new account will be initialized with default and then added to the list of accounts + * + * @param {object} [details] + * the accounts details like the name, hostname, port and username as key value pairs. + * + * @returns {SieveAccounts} + * a self reference. + */ + async create(details) { + + // create a unique id; + + const id = this.generateId(); + + this.accounts[id] = new SieveAccount(id); + + await this.save(); + + if (typeof (details) === "undefined" || details === null) + return this; + + if ((details.hostname !== null) && (details.hostname !== undefined)) + await (await this.accounts[id].getHost()).setHostname(details.hostname); + + if ((details.port !== null) && (details.port !== undefined)) + await (await this.accounts[id].getHost()).setPort(details.port); + + if ((details.username !== null) && (details.username !== undefined)) + await (await this.accounts[id].getAuthentication()).setUsername(details.username); + + if ((details.name !== null) && (details.name !== undefined)) + await (await this.accounts[id].getHost()).setDisplayName(details.name); + + return this; + } + + /** + * Removes the account including all settings. + * + * @param {AccountId} id + * the unique id which identifies the account. + * @returns {SieveAccounts} + * a self reference + */ + async remove(id) { + // remove the accounts... + delete this.accounts[id]; + // ... an persist it. + await this.save(); + + // remove the account's settings. + (new SievePrefManager(`@${id}`)).clear(); + + return this; + } + + /** + * Imports previously exported account settings. + * + * @param {string} data + * the settings to be imported. + */ + async import(data) { + data = JSON.parse(data); + + if (data.version !== SETTINGS_VERSION_I) + throw new Error(`Unknown version ${data.version}`); + + const details = { + name: data.settings["host.displayName"], + hostname: data.settings["hostname"], + port: data.settings["port"], + username: data.settings["authentication.username"] + }; + + await this.create(details); + } + + /** + * Exports the account's settings. + * + * @param {string} id + * the unique account id. + * + * @returns {string} + * the account settings as json string. + */ + async export(id) { + const config = new SievePrefManager(`@${id}`); + + let data = {}; + for (const key of config.getKeys()) + data[key] = await (config.getValue(key)); + + data = { + "version": SETTINGS_VERSION_I, + "settings": data + }; + + return JSON.stringify( + data, null, JSON_INDENTATION); + } +} + +export { SieveAccounts }; diff --git a/src/app/libs/managesieve.ui/settings/logic/SieveAuthentication.mjs b/src/app/libs/managesieve.ui/settings/logic/SieveAuthentication.mjs new file mode 100644 index 00000000..63c2267f --- /dev/null +++ b/src/app/libs/managesieve.ui/settings/logic/SieveAuthentication.mjs @@ -0,0 +1,182 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + + +import { SieveAbstractAuthentication } from "./SieveAbstractAuthentication.mjs"; + +import { SieveIpcClient } from "./../../utils/SieveIpcClient.mjs"; + +const KEY_USERNAME = "authentication.username"; + +/** + * Prompts for a password. + */ +class SieveElectronAuthentication extends SieveAbstractAuthentication { + + /** + * @inheritdoc + */ + constructor(account) { + super(account); + this.canStore = null; + } + /** + * Sets the username. + * + * @param {string} username + * the username as string, can not be null. + * + */ + async setUsername(username) { + await this.account.getConfig().setString(KEY_USERNAME, username); + } + + /** + * @inheritdoc + */ + async getUsername() { + return await this.account.getConfig().getString(KEY_USERNAME); + } + + /** + * @inheritdoc + */ + hasPassword() { + return true; + } + + /** + * Checks if the system key store can be accessed from this app. + * It could be unavailable due to missing libraries or because + * it has been disabled by the IT administrator. + * + * @returns {boolean} + * true in case a password can be stored otherwise false. + */ + async canStorePassword() { + if (typeof (this.canStore) === "undefined" || this.canStore === null) { + this.canStore = await SieveIpcClient.sendMessage("core", "keystore-ready", "", window); + } + + return (this.canStore === true); + } + + /** + * Forgets any passwords remembered for this username. + * It will fail silently. + */ + async forget() { + try { + if (!await this.canStorePassword()) + return; + + const username = await this.getUsername(); + + await SieveIpcClient.sendMessage( + "core", "keystore-forget", { "username": username }, window); + + } catch (ex) { + this.account.getLogger().logAction("Forgetting password failed " + ex); + } + } + + /** + * Stores a password in the system wide key store. + * It will fail silently in case storing the password failed. + * + * @param {string} password + * the password to be stored. + */ + async setStoredPassword(password) { + try { + if (! await this.canStorePassword()) + return; + + const username = await this.getUsername(); + + await SieveIpcClient.sendMessage( + "core", "keystore-store", { "username": username, "password": password }, window); + + } catch (ex) { + this.account.getLogger().logAction("Storing password failed " + ex); + } + } + + /** + * Gets the stored password + * + * @returns {string} + * the password or null in case it does not exist. + */ + async getStoredPassword() { + try { + if (!await this.canStorePassword()) + return null; + + const username = await this.getUsername(); + + return await SieveIpcClient.sendMessage( + "core", "keystore-get", { "username": username }, window); + + } catch (ex) { + this.account.getLogger().logAction("Getting password failed " + ex); + } + + return null; + } + + /** + * Checks if the password is stored in the cert store. + * + * @returns {boolean} + * true in case a password is stored. + */ + async hasStoredPassword() { + try { + if (!await this.canStorePassword()) + return false; + + if (await this.getStoredPassword() === null) + return false; + + return true; + } catch (ex) { + this.account.getLogger().logAction("Checking for stored password failed " + ex); + } + + return false; + } + + /** + * @inheritdoc + */ + async getPassword() { + + if (await this.hasStoredPassword()) + return await this.getStoredPassword(); + + const request = { + "username": await this.getUsername(), + "displayname": await (await this.account.getHost()).getDisplayName(), + "remember": await this.canStorePassword() + }; + + const credentials = await SieveIpcClient.sendMessage( + "accounts", "account-show-authentication", request); + + if (credentials.remember) + await this.setStoredPassword(credentials.password); + + return credentials.password; + } +} + +export { SieveElectronAuthentication as SieveAuthentication }; diff --git a/src/app/libs/managesieve.ui/settings/logic/SieveAuthenticationSettings.js b/src/app/libs/managesieve.ui/settings/logic/SieveAuthenticationSettings.js deleted file mode 100644 index d5bd53a7..00000000 --- a/src/app/libs/managesieve.ui/settings/logic/SieveAuthenticationSettings.js +++ /dev/null @@ -1,247 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - const AUTH_TYPE_PROMPT = 0; - const DEFAULT_AUTH_TYPE = AUTH_TYPE_PROMPT; - - const CONFIG_AUTHENTICATION_TYPE = "activeLogin"; - - const { SieveAbstractAuthentication } = require("./SieveAbstractAuthentication.js"); - const { SieveAbstractMechanism } = require("./SieveAbstractMechanism.js"); - - const { SieveIpcClient } = require("./../../utils/SieveIpcClient.js"); - - let keytar = null; - - /** - * Prompts for a password. - */ - class SievePromptAuthentication extends SieveAbstractAuthentication { - - /** - * Sets the username. - * - * @param {string} username - * the username as string, can not be null. - * - */ - async setUsername(username) { - await this.account.getConfig().setString("authentication.username", username); - } - - /** - * @inheritdoc - */ - async getUsername() { - return await this.account.getConfig().getString("authentication.username"); - } - - /** - * @inheritdoc - */ - hasPassword() { - return true; - } - - /** - * Checks if the system key store can be accessed from this app. - * It could be unavailable due to missing libraries or because - * it has been disabled by the IT administrator. - * - * @returns {boolean} - * true in case a password can be stored otherwise false. - */ - canStorePassword() { - const keystore = this.getKeyStore(); - - if (typeof (keystore) === "undefined" || keystore === null) - return false; - - return true; - } - - /** - * Returns the system wide keystore. - * There is no guarantee that it exists. - * It may fail due to missing external dependencies or because it was - * just disabled by the administrator. - * - * @returns {Keytar} - * the keytar object. - */ - getKeyStore() { - if (typeof (keytar) !== "undefined" && keytar !== null) - return keytar; - - try { - keytar = require("./../../../keytar"); - } catch (ex) { - this.account.getLogger().logAction("Could not initialize keystore: " + ex); - } - - return keytar; - } - - /** - * Forgets any passwords remembered for this username. - * It will fail silently. - */ - async forget() { - try { - if (!this.canStorePassword()) - return; - - const username = await this.getUsername(); - await this.getKeyStore().deletePassword("Sieve Editor", username); - } catch (ex) { - this.account.getLogger().logAction("Forgetting password failed " + ex); - } - } - - /** - * Stores a password in the system wide key store. - * It will fail silently in case storing the password failed. - * - * @param {string} password - * the password to be stored. - */ - async setStoredPassword(password) { - try { - if (!this.canStorePassword()) - return; - - const username = await this.getUsername(); - await this.getKeyStore().setPassword("Sieve Editor", username, password); - } catch (ex) { - this.account.getLogger().logAction("Storing password failed " + ex); - } - } - - /** - * Gets the stored password - * - * @returns {string} - * the password or null in case it does not exist. - */ - async getStoredPassword() { - try { - if (!this.canStorePassword()) - return null; - - const username = await this.getUsername(); - return await this.getKeyStore().getPassword("Sieve Editor", username); - } catch (ex) { - this.account.getLogger().logAction("Getting password failed " + ex); - } - - return null; - } - - /** - * Checks if the password is stored in the cert store. - * - * @returns {boolean} - * true in case a password is stored. - */ - async hasStoredPassword() { - try { - if (!this.canStorePassword()) - return false; - - const username = await this.getUsername(); - - if (await this.getKeyStore().getPassword("Sieve Editor", username) === null) - return false; - - return true; - } catch (ex) { - this.account.getLogger().logAction("Checking for stored password failed " + ex); - } - - return false; - } - - /** - * @inheritdoc - */ - async getPassword() { - - if (await this.hasStoredPassword()) - return await this.getStoredPassword(); - - const request = { - "username": await this.getUsername(), - "displayname": await (await this.account.getHost()).getDisplayName(), - "remember": this.canStorePassword() - }; - - const credentials = await SieveIpcClient.sendMessage( - "accounts", "account-show-authentication", request); - - if (credentials.remember) - await this.setStoredPassword(credentials.password); - - return credentials.password; - } - } - - /** - * Manages the authorization settings. - */ - class SieveAuthentication extends SieveAbstractMechanism { - - /** - * @inheritdoc - **/ - getDefault() { - return DEFAULT_AUTH_TYPE; - } - - /** - * @inheritdoc - **/ - getKey() { - return CONFIG_AUTHENTICATION_TYPE; - } - - /** - * @inheritdoc - **/ - hasMechanism(type) { - switch (type) { - case AUTH_TYPE_PROMPT: - return true; - - default: - return false; - } - } - - /** - * @inheritdoc - **/ - getMechanismById(type) { - switch (type) { - case AUTH_TYPE_PROMPT: - // fall through we just implement prompt authentication - default: - return new SievePromptAuthentication(AUTH_TYPE_PROMPT, this.account); - } - } - } - - exports.SieveAuthentication = SieveAuthentication; - -})(module.exports); diff --git a/src/app/libs/managesieve.ui/settings/logic/SieveAuthorization.mjs b/src/app/libs/managesieve.ui/settings/logic/SieveAuthorization.mjs new file mode 100644 index 00000000..aafe83c9 --- /dev/null +++ b/src/app/libs/managesieve.ui/settings/logic/SieveAuthorization.mjs @@ -0,0 +1,112 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +const CONFIG_AUTHORIZATION_TYPE_NONE = 0; +const CONFIG_AUTHORIZATION_TYPE_SIEVE = 1; +const CONFIG_AUTHORIZATION_TYPE_PROMPT = 2; +const CONFIG_AUTHORIZATION_TYPE_CUSTOM = 3; + +const DEFAULT_AUTHORIZATION_TYPE = CONFIG_AUTHORIZATION_TYPE_SIEVE; + +const CONFIG_AUTHORIZATION_TYPE = "authorization.type"; + +import { + SieveNoAuthorization, + SieveCustomAuthorization, + SieveDefaultAuthorization, + SieveAbstractAuthorization +} from "./SieveAbstractAuthorization.mjs"; + +import { SieveAbstractMechanism } from "./SieveAbstractMechanism.mjs"; +import { SieveIpcClient } from "./../../utils/SieveIpcClient.mjs"; + +/** + * Shows a dialog and prompts for the authorization. + */ +class SievePromptAuthorization extends SieveAbstractAuthorization { + + /** + * @inheritdoc + */ + getType() { + return CONFIG_AUTHORIZATION_TYPE_PROMPT; + } + + /** + * Shows a dialog asking for the authorization. + * @returns {string} + * the authorization string or null in case the dialog was canceled. + */ + async getAuthorization() { + const name = await (await this.account.getHost()).getDisplayName(); + + return await SieveIpcClient.sendMessage( + "accounts", "account-show-authorization", + { "displayname": name }); + } +} + +/** + * Manages the authorization settings. + */ +class SieveAuthorization extends SieveAbstractMechanism { + + /** + * @inheritdoc + **/ + getDefault() { + return DEFAULT_AUTHORIZATION_TYPE; + } + + /** + * @inheritdoc + **/ + getKey() { + return CONFIG_AUTHORIZATION_TYPE; + } + + /** + * @inheritdoc + **/ + hasMechanism(type) { + switch (type) { + case CONFIG_AUTHORIZATION_TYPE_NONE: + case CONFIG_AUTHORIZATION_TYPE_SIEVE: + case CONFIG_AUTHORIZATION_TYPE_PROMPT: + case CONFIG_AUTHORIZATION_TYPE_CUSTOM: + return true; + + default: + return false; + } + } + + /** + * @inheritdoc + **/ + getMechanismById(type) { + switch (type) { + case CONFIG_AUTHORIZATION_TYPE_NONE: + return new SieveNoAuthorization(CONFIG_AUTHORIZATION_TYPE_NONE, this.account); + case CONFIG_AUTHORIZATION_TYPE_SIEVE: + return new SieveDefaultAuthorization(CONFIG_AUTHORIZATION_TYPE_SIEVE, this.account); + case CONFIG_AUTHORIZATION_TYPE_PROMPT: + return new SievePromptAuthorization(CONFIG_AUTHORIZATION_TYPE_PROMPT, this.account); + case CONFIG_AUTHORIZATION_TYPE_CUSTOM: + return new SieveCustomAuthorization(CONFIG_AUTHORIZATION_TYPE_CUSTOM, this.account); + + default: + throw new Error("Unknown authorization mechanism"); + } + } +} + +export { SieveAuthorization }; diff --git a/src/app/libs/managesieve.ui/settings/logic/SieveAuthorizationSettings.js b/src/app/libs/managesieve.ui/settings/logic/SieveAuthorizationSettings.js deleted file mode 100644 index c77ddc80..00000000 --- a/src/app/libs/managesieve.ui/settings/logic/SieveAuthorizationSettings.js +++ /dev/null @@ -1,119 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - const CONFIG_AUTHORIZATION_TYPE_NONE = 0; - const CONFIG_AUTHORIZATION_TYPE_SIEVE = 1; - const CONFIG_AUTHORIZATION_TYPE_PROMPT = 2; - const CONFIG_AUTHORIZATION_TYPE_CUSTOM = 3; - - const DEFAULT_AUTHORIZATION_TYPE = CONFIG_AUTHORIZATION_TYPE_SIEVE; - - const CONFIG_AUTHORIZATION_TYPE = "authorization.type"; - - const { - SieveNoAuthorization, - SieveCustomAuthorization, - SieveDefaultAuthorization, - SieveAbstractAuthorization - } = require("./SieveAbstractAuthorization.js"); - - const { SieveAbstractMechanism } = require("./SieveAbstractMechanism.js"); - - const { SieveIpcClient} = require("./../../utils/SieveIpcClient.js"); - - /** - * Shows a dialog and prompts for the authorization. - */ - class SievePromptAuthorization extends SieveAbstractAuthorization { - - /** - * @inheritdoc - */ - getType() { - return CONFIG_AUTHORIZATION_TYPE_PROMPT; - } - - /** - * Shows a dialog asking for the authorization. - * @returns {string} - * the authorization string or null in case the dialog was canceled. - */ - async getAuthorization() { - const name = await (await this.account.getHost()).getDisplayName(); - - return await SieveIpcClient.sendMessage( - "accounts", "account-show-authorization", - { "displayname": name }); - } - } - - /** - * Manages the authorization settings. - */ - class SieveAuthorization extends SieveAbstractMechanism { - - /** - * @inheritdoc - **/ - getDefault() { - return DEFAULT_AUTHORIZATION_TYPE; - } - - /** - * @inheritdoc - **/ - getKey() { - return CONFIG_AUTHORIZATION_TYPE; - } - - /** - * @inheritdoc - **/ - hasMechanism(type) { - switch (type) { - case CONFIG_AUTHORIZATION_TYPE_NONE: - case CONFIG_AUTHORIZATION_TYPE_SIEVE: - case CONFIG_AUTHORIZATION_TYPE_PROMPT: - case CONFIG_AUTHORIZATION_TYPE_CUSTOM: - return true; - - default: - return false; - } - } - - /** - * @inheritdoc - **/ - getMechanismById(type) { - switch (type) { - case CONFIG_AUTHORIZATION_TYPE_NONE: - return new SieveNoAuthorization(CONFIG_AUTHORIZATION_TYPE_NONE, this.account); - case CONFIG_AUTHORIZATION_TYPE_SIEVE: - return new SieveDefaultAuthorization(CONFIG_AUTHORIZATION_TYPE_SIEVE, this.account); - case CONFIG_AUTHORIZATION_TYPE_PROMPT: - return new SievePromptAuthorization(CONFIG_AUTHORIZATION_TYPE_PROMPT, this.account); - case CONFIG_AUTHORIZATION_TYPE_CUSTOM: - return new SieveCustomAuthorization(CONFIG_AUTHORIZATION_TYPE_CUSTOM, this.account); - - default: - throw new Error("Unknown authorization mechanism"); - } - } - } - - exports.SieveAuthorization = SieveAuthorization; - -})(module.exports); diff --git a/src/app/libs/managesieve.ui/settings/logic/SieveHost.mjs b/src/app/libs/managesieve.ui/settings/logic/SieveHost.mjs new file mode 100644 index 00000000..4b10db7d --- /dev/null +++ b/src/app/libs/managesieve.ui/settings/logic/SieveHost.mjs @@ -0,0 +1,145 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +const CONFIG_KEEP_ALIVE_INTERVAL = "keepalive"; +// eslint-disable-next-line no-magic-numbers +const ONE_MINUTE = 60 * 1000; +// eslint-disable-next-line no-magic-numbers +const FIVE_MINUTES = 5 * ONE_MINUTE; + +import { SieveCustomHost } from "./SieveAbstractHost.mjs"; + +/** + * Extends the CustomHost implementation by a display name and fingerprint setting + **/ +class SieveElectronHost extends SieveCustomHost { + + /** + * @inheritdoc + **/ + async getHostname() { + return await this.account.getConfig().getString("hostname", ""); + } + + /** + * Sets the custom hostname which shall be used. + * + * @param {string} hostname + * the hostname or ip as string. + * + * @returns {SieveElectronHost} + * a self reference + */ + async setHostname(hostname) { + await this.account.getConfig().setString("hostname", hostname); + return this; + } + + /** + * @inheritdoc + **/ + async getDisplayName() { + return await this.account.getConfig().getString("host.displayName", "Unnamed Account"); + } + + /** + * Sets the account display name. + * @param {string} value + * sets the account's display name + * @returns {SieveCustomHostEx} + * a self reference + */ + async setDisplayName(value) { + await this.account.getConfig().setString("host.displayName", value); + return this; + } + + /** + * Configures the maximum idle time after a message is send. + * In case the time span elapsed an keep alive message will be + * send to the server. + * + * @param {int} value + * the maximal time in seconds. zero disables keep alive messages + * + * @returns {SieveAbstractHost} + * a self reference + */ + async setKeepAlive(value) { + await this.account.getConfig().setInteger(CONFIG_KEEP_ALIVE_INTERVAL, value); + return this; + } + + /** + * @inheritdoc + */ + async getKeepAlive() { + return await this.account.getConfig().getInteger(CONFIG_KEEP_ALIVE_INTERVAL, FIVE_MINUTES); + } + + /** + * Each certificate has a unique fingerprint. + * + * Normally this fingerprint is not used directly. + * But in case no chain of trust can be established, + * the typical fallback is to verify the fingerprint. + * + * This is normal case for a self signed certificate. + * + * @returns {string} + * the accounts fingerprint or an empty string in case no fingerprint is stored. + **/ + async getFingerprint() { + return await this.account.getConfig().getString("host.fingerprint", ""); + } + + /** + * Sets the account's fingerprint. + * + * @param {string} value + * the accounts fingerprint, or pass an empty string to disable. + * @returns {SieveCustomHostEx} + * a self reference + */ + async setFingerprint(value) { + await this.account.getConfig().setString("host.fingerprint", value); + return this; + } + + /** + * Gets the certificate errors to ignore. + * + * @returns {string} + * the node js error code to ignore as string or an empty string. + */ + async getIgnoreCertErrors() { + return await this.account.getConfig().getString("host.ignoreCertErrors", ""); + } + + /** + * Defines which certificate error code should be ignored. + * + * In general it is not a good idea to ignore certificate errors. + * But there are some exception: e.g. a self signed error + * after you verified the certificates fingerprint. + * + * @param {string} errorCode + * the node js error code or an empty string to disable. + * @returns {SieveCustomHostEx} + * a self reference + */ + async setIgnoreCertErrors(errorCode) { + await this.account.getConfig().setString("host.ignoreCertErrors", errorCode); + return this; + } +} + +export { SieveElectronHost as SieveHost }; diff --git a/src/app/libs/managesieve.ui/settings/logic/SieveHostSettings.js b/src/app/libs/managesieve.ui/settings/logic/SieveHostSettings.js deleted file mode 100644 index bff61c1f..00000000 --- a/src/app/libs/managesieve.ui/settings/logic/SieveHostSettings.js +++ /dev/null @@ -1,155 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - const HOST_TYPE_CUSTOM = 1; - - const CONFIG_HOST_TYPE = "activeHost"; - - const { SieveAbstractMechanism } = require("./SieveAbstractMechanism.js"); - const { SieveCustomHost } = require("./SieveAbstractHost.js"); - - /** - * Extends the CustomHost implementation by a display name and fingerprint setting - **/ - class SieveCustomHostEx extends SieveCustomHost { - - /** - * The human readable display name for this account. - * It can be any valid javascript string. - * - * @returns {string} - * the display name - **/ - async getDisplayName() { - return await this.account.getConfig().getString("host.displayName", "Unnamed Account"); - } - - /** - * Sets the account display name. - * @param {string} value - * sets the account's display name - * @returns {SieveCustomHostEx} - * a self reference - */ - async setDisplayName(value) { - await this.account.getConfig().setString("host.displayName", value); - return this; - } - - /** - * Each certificate has a unique fingerprint. - * - * Normally this fingerprint is not used directly. - * But in case no chain of trust can be established, - * the typical fallback is to verify the fingerprint. - * - * This is normal case for a self signed certificate. - * - * @returns {string} - * the accounts fingerprint or an empty string in case no fingerprint is stored. - **/ - async getFingerprint() { - return await this.account.getConfig().getString("host.fingerprint", ""); - } - - /** - * Sets the account's fingerprint. - * - * @param {string} value - * the accounts fingerprint, or pass an empty string to disable. - * @returns {SieveCustomHostEx} - * a self reference - */ - async setFingerprint(value) { - await this.account.getConfig().setString("host.fingerprint", value); - return this; - } - - /** - * Gets the certificate errors to ignore. - * - * @returns {string} - * the node js error code to ignore as string or an empty string. - */ - async getIgnoreCertErrors() { - return await this.account.getConfig().getString("host.ignoreCertErrors", ""); - } - - /** - * Defines which certificate error code should be ignored. - * - * In general it is not a good idea to ignore certificate errors. - * But there are some exception: e.g. a self signed error - * after you verified the certificates fingerprint. - * - * @param {string} errorCode - * the node js error code or an empty string to disable. - * @returns {SieveCustomHostEx} - * a self reference - */ - async setIgnoreCertErrors(errorCode) { - await this.account.getConfig().setString("host.ignoreCertErrors", errorCode); - return this; - } - } - - /** - * A transparent wrapper needed to deal with the different - * host mechanism which are provided by electron and thunderbird. - **/ - class SieveHost extends SieveAbstractMechanism { - - /** - * @inheritdoc - **/ - getKey() { - return CONFIG_HOST_TYPE; - } - - /** - * @inheritdoc - **/ - getDefault() { - return HOST_TYPE_CUSTOM; - } - - /** - * @inheritdoc - */ - hasMechanism(type) { - switch (type) { - case HOST_TYPE_CUSTOM: - return true; - - default: - return false; - } - } - - /** - * @inheritdoc - */ - getMechanismById(type) { - - switch (type) { - default: - return new SieveCustomHostEx(HOST_TYPE_CUSTOM, this.account); - } - } - } - - exports.SieveHost = SieveHost; - -})(module.exports); diff --git a/src/app/libs/managesieve.ui/settings/logic/SievePrefManager.js b/src/app/libs/managesieve.ui/settings/logic/SievePrefManager.js deleted file mode 100644 index b01f05ce..00000000 --- a/src/app/libs/managesieve.ui/settings/logic/SievePrefManager.js +++ /dev/null @@ -1,87 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - const { SieveAbstractPrefManager } = require("./SieveAbstractPrefManager.js"); - - /** - * Manages preferences. - * It uses the DOM's local storage interface - */ - class SievePrefManager extends SieveAbstractPrefManager { - - /** - * Returns all the keys contained by this namespace. - * The keys are returned without any namespace prefix. - * - * @returns {Set} - * a set with all key names. - */ - getKeys() { - const keys = new Set(); - - const namespace = `${this.getNamespace()}.`; - - for (let idx = 0; idx < localStorage.length; idx++) { - const key = localStorage.key(idx); - if (key.startsWith(namespace)) - keys.add(key.substring(namespace.length)); - } - - return keys; - } - - /** - * Clears the complete name space. - */ - clear() { - const keys = this.getKeys(); - - for (const key of keys) - localStorage.removeItem(`${this.getNamespace()}.${key}`); - } - - /** - * Returns a specific value. - * @param {string} key - * the key which should be returned. - * @returns {object} - * the value or undefined in case it does not exist. - */ - async getValue(key) { - return await localStorage.getItem(`${this.getNamespace()}.${key}`); - } - - /** - * Sets and persists the given preference. - * - * @param {string} key - * the preference key which should be written. - * @param {object} value - * the key's value. - * @returns {SievePrefManager} - * a self reference. - */ - async setValue(key, value) { - await localStorage.setItem(`${this.getNamespace()}.${key}`, value); - return this; - } - } - - if (typeof(module) !== "undefined" && module && module.exports) - module.exports.SievePrefManager = SievePrefManager; - else - exports.SievePrefManager = SievePrefManager; - -})(this); diff --git a/src/app/libs/managesieve.ui/settings/logic/SievePrefManager.mjs b/src/app/libs/managesieve.ui/settings/logic/SievePrefManager.mjs new file mode 100644 index 00000000..81255305 --- /dev/null +++ b/src/app/libs/managesieve.ui/settings/logic/SievePrefManager.mjs @@ -0,0 +1,79 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + + +import { SieveAbstractPrefManager } from "./SieveAbstractPrefManager.mjs"; + +/** + * Manages preferences. + * It uses the DOM's local storage interface + */ +class SieveElectronPrefManager extends SieveAbstractPrefManager { + + /** + * Returns all the keys contained by this namespace. + * The keys are returned without any namespace prefix. + * + * @returns {Set} + * a set with all key names. + */ + getKeys() { + const keys = new Set(); + + const namespace = `${this.getNamespace()}.`; + + for (let idx = 0; idx < localStorage.length; idx++) { + const key = localStorage.key(idx); + if (key.startsWith(namespace)) + keys.add(key.substring(namespace.length)); + } + + return keys; + } + + /** + * Clears the complete name space. + */ + clear() { + const keys = this.getKeys(); + + for (const key of keys) + localStorage.removeItem(`${this.getNamespace()}.${key}`); + } + + /** + * Returns a specific value. + * @param {string} key + * the key which should be returned. + * @returns {object} + * the value or undefined in case it does not exist. + */ + async getValue(key) { + return await localStorage.getItem(`${this.getNamespace()}.${key}`); + } + + /** + * Sets and persists the given preference. + * + * @param {string} key + * the preference key which should be written. + * @param {object} value + * the key's value. + * @returns {SievePrefManager} + * a self reference. + */ + async setValue(key, value) { + await localStorage.setItem(`${this.getNamespace()}.${key}`, value); + return this; + } +} + +export { SieveElectronPrefManager as SievePrefManager }; diff --git a/src/app/libs/managesieve.ui/settings/logic/SieveSecurity.mjs b/src/app/libs/managesieve.ui/settings/logic/SieveSecurity.mjs new file mode 100644 index 00000000..46e43ec2 --- /dev/null +++ b/src/app/libs/managesieve.ui/settings/logic/SieveSecurity.mjs @@ -0,0 +1,66 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +const PREF_MECHANISM = "security.mechanism"; +const PREF_TLS = "security.tls"; + +import { SieveAbstractSecurity } from "./SieveAbstractSecurity.mjs"; + +/** + * Manages the account's security related settings + */ +class SieveSecurity extends SieveAbstractSecurity { + + /** + * @inheritdoc + */ + async getMechanism() { + return await this.account.getConfig().getString(PREF_MECHANISM, "default"); + } + + /** + * Sets the sasl mechanism. + * + * @param {string} mechanism + * the sasl mechanism which should be used. + * + * @returns {SieveSecurity} + * a self reference + */ + async setMechanism(mechanism) { + await this.account.getConfig().setString(PREF_MECHANISM, mechanism); + return this; + } + + /** + * @inheritdoc + */ + async isSecure() { + return await this.account.getConfig().getBoolean(PREF_TLS, true); + } + + /** + * Defines if a secure connections shall be used. + * + * @param {boolean} value + * set to true for a secure connection. + * + * @returns {SieveSecurity} + * a self reference + */ + async setSecure(value) { + await this.account.getConfig().setBoolean(PREF_TLS, value); + return this; + } + +} + +export { SieveSecurity }; diff --git a/src/app/libs/managesieve.ui/settings/ui/SieveCredentialSettingsUI.js b/src/app/libs/managesieve.ui/settings/ui/SieveCredentialSettingsUI.js deleted file mode 100644 index 04890b65..00000000 --- a/src/app/libs/managesieve.ui/settings/ui/SieveCredentialSettingsUI.js +++ /dev/null @@ -1,364 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - /* global bootstrap */ - /* global SieveTemplate */ - - /** - * A UI renderer for the sieve settings dialog - */ - class SieveCredentialsSettingsUI { - - /** - * Initializes the settings - * @param {SieveAccount} account - * the account for which the settings edited. - */ - constructor(account) { - this.account = account; - } - - /** - * Sets the authentication type - * - * @param {string} type - * the authentication type which should be used - * @returns {SieveCredentialsUI} - * a self reference - */ - setSaslMechanism(type) { - - const parent = this.getDialog(); - - const text = parent - .querySelector(".sieve-settings-authentication") - .querySelector(`.dropdown-item[data-sieve-authentication="${type}"]`) - .textContent; - - parent - .querySelector(".sieve-settings-authentication button") - .dataset.sieveAuthentication = type; - - parent - .querySelector(".sieve-settings-authentication button") - .textContent = text; - - return this; - } - - /** - * The authentication type which was selected - * - * @returns {string} - * authentication type - */ - getSaslMechanism() { - return this.getDialog() - .querySelector(".sieve-settings-authentication button") - .dataset.sieveAuthentication; - } - - /** - * Sets the username in the ui - * - * @param {string} username - * the username which should be set - * @returns {SieveSettingsUI} - * a self reference - */ - setAuthentication(username) { - this.getDialog() - .querySelector(".sieve-settings-username").value = username; - - return this; - } - - /** - * Gets the username from the ui. - * - * @returns {string} - * the username as string. - */ - getAuthentication() { - return this.getDialog() - .querySelector(".sieve-settings-username").value; - } - - /** - * Selects the given authorization type in the dropdown control. - * - * @param {int|string} type - * the authorization type to be activated. - * @returns {SieveCredentialsSettingsUI} - * a self reference. - */ - setAuthorizationType(type) { - const dialog = this.getDialog(); - - const text = dialog - .querySelector(".sieve-settings-authorization") - .querySelector(`.dropdown-item[data-sieve-authorization="${type}"]`) - .textContent; - - dialog - .querySelector(".sieve-settings-authorization button") - .dataset.sieveAuthorization = type; - - dialog - .querySelector(".sieve-settings-authorization button") - .textContent = text; - - if (`${type}` === "3") - dialog.querySelector(".sieve-settings-authorization-username").classList.remove("d-none"); - else - dialog.querySelector(".sieve-settings-authorization-username").classList.add("d-none"); - - return this; - } - - /** - * Gets the current authorization type set in the dropdown menu. - * - * @returns {string} - * the authorization type as string. - */ - getAuthorizationType() { - return this.getDialog() - .querySelector(".sieve-settings-authorization button") - .dataset.sieveAuthorization; - } - - /** - * Sets the authorization name. - * Authorization allows an authenticated user (normally admin) - * to access an other users sieve account. - * - * @param {string} username - * the username as which the current user should authorized - * @returns {SieveSettingsUI} - * a self reference - */ - setAuthorization(username) { - this.getDialog() - .querySelector(".sieve-settings-text-authorization-username").value = username; - - return this; - } - - /** - * Gets the authorization name from the ui. - * - * @returns {string} - * the authorized username. - */ - getAuthorization() { - return this.getDialog() - .querySelector(".sieve-settings-text-authorization-username").value; - } - - /** - * Gets the current dialogs encryption settings. - * - * @returns {boolean} - * true in case an encrypted connection should be used otherwise false. - */ - isEncrypted() { - - if (this.getDialog().querySelector("#sieve-settings-encryption-off").checked) - return false; - - return true; - } - - /** - * Sets the encryption settings in the current dialog. - * - * @param {boolean} encrypted - * the encryption status to set. False in case encryption is disabled - * otherwise it will be enabled - * @returns {SieveServerSettingsUI} - * a self reference - */ - setEncrypted(encrypted) { - const parent = this.getDialog(); - - if (encrypted === false) - parent.querySelector("#sieve-settings-encryption-off").checked = true; - else - parent.querySelector("#sieve-settings-encryption-on").checked = true; - - return this; - } - - /** - * Shows the advanced setting - */ - showAdvanced() { - const parent = this.getDialog(); - - parent.querySelector(".siv-settings-advanced").classList.remove("d-none"); - parent.querySelector(".siv-settings-show-advanced").classList.add("d-none"); - parent.querySelector(".siv-settings-hide-advanced").classList.remove("d-none"); - } - - /** - * Hides the advanced settings - */ - hideAdvanced() { - const parent = this.getDialog(); - - parent.querySelector(".siv-settings-advanced").classList.add("d-none"); - parent.querySelector(".siv-settings-show-advanced").classList.remove("d-none"); - parent.querySelector(".siv-settings-hide-advanced").classList.add("d-none"); - } - - /** - * Shows the settings dialog - * - * @returns {boolean} - * true in case new settings where applied. - * false in case the dialog was canceled. - */ - async show() { - - document.querySelector("#ctx").appendChild( - await (new SieveTemplate()).load("./settings/ui/settings.credentials.tpl")); - - await this.render(); - - const dialog = this.getDialog(); - const modal = new bootstrap.Modal(dialog); - - modal.show(); - - dialog - .querySelector(".sieve-settings-apply") - .addEventListener("click", async () => { - await this.save(); - modal.hide(); - }); - - return await new Promise((resolve) => { - - dialog.addEventListener('hidden.bs.modal', () => { - modal.dispose(); - dialog.parentElement.removeChild(dialog); - - resolve(); - }); - - }); - } - - /** - * Validates and saves the setting before closing the dialog. - * In case the settings are invalid an error message is displayed. - */ - async save() { - - const settings = { - general: { - secure: this.isEncrypted(), - sasl: this.getSaslMechanism() - }, - authentication: { - username: this.getAuthentication(), - mechanism: 0 - }, - authorization: { - username: this.getAuthorization(), - mechanism: this.getAuthorizationType() - } - }; - - await this.account.send("account-settings-set-credentials", settings); - } - - /** - * Returns the currents dialogs UI Element. - * - * @returns {object} - * the dialogs UI elements. - */ - getDialog() { - return document.querySelector("#dialog-settings-credentials"); - } - - /** - * Renders the UI element into the dom. - */ - async render() { - const parent = this.getDialog(); - - const credentials = await this.account.send("account-setting-get-credentials"); - - // Authentication settings - this.setEncrypted(credentials.general.secure); - this.setSaslMechanism(credentials.general.sasl); - - this.setAuthentication(credentials.authentication.username); - - parent - .querySelectorAll(".sieve-settings-authentication .dropdown-item") - .forEach((item) => { - item.addEventListener("click", (event) => { - this.setSaslMechanism(event.target.dataset.sieveAuthentication); - }); - }); - - // Show the forget password button only when a password is stored. - if (credentials.authentication.stored) - parent.querySelector(".sieve-settings-forget-password").classList.remove("d-none"); - else - parent.querySelector(".sieve-settings-forget-password").classList.add("d-none"); - - parent - .querySelector(".sieve-settings-forget-password button") - .addEventListener("click", async () => { - await this.account.send("account-settings-forget-credentials"); - - this.getDialog() - .querySelector(".sieve-settings-forget-password").classList.add("d-none"); - }); - - // Authorization settings.... - this.setAuthorizationType(credentials.authorization.type); - this.setAuthorization(credentials.authorization.username); - - parent - .querySelectorAll(".sieve-settings-authorization .dropdown-item") - .forEach((item) => { - item.addEventListener("click", (event) => { - this.setAuthorizationType(event.target.dataset.sieveAuthorization); - }); - }); - - parent - .querySelector(".siv-settings-show-advanced") - .addEventListener("click", () => { this.showAdvanced(); }); - - parent - .querySelector(".siv-settings-hide-advanced") - .addEventListener("click", () => { this.hideAdvanced(); }); - - this.hideAdvanced(); - } - } - if (typeof (module) !== "undefined" && module !== null && module.exports) - module.exports = SieveCredentialsSettingsUI; - else - exports.SieveCredentialsSettingsUI = SieveCredentialsSettingsUI; - -})(this); diff --git a/src/app/libs/managesieve.ui/settings/ui/SieveCredentialSettingsUI.mjs b/src/app/libs/managesieve.ui/settings/ui/SieveCredentialSettingsUI.mjs new file mode 100644 index 00000000..18ab58d8 --- /dev/null +++ b/src/app/libs/managesieve.ui/settings/ui/SieveCredentialSettingsUI.mjs @@ -0,0 +1,357 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +/* global bootstrap */ + +import { SieveTemplate } from "./../../utils/SieveTemplate.mjs"; + +/** + * A UI renderer for the sieve settings dialog + */ +class SieveCredentialsSettingsUI { + + /** + * Initializes the settings + * @param {SieveAccount} account + * the account for which the settings edited. + */ + constructor(account) { + this.account = account; + } + + /** + * Sets the authentication type + * + * @param {string} type + * the authentication type which should be used + * @returns {SieveCredentialsUI} + * a self reference + */ + setSaslMechanism(type) { + + const parent = this.getDialog(); + + const text = parent + .querySelector(".sieve-settings-authentication") + .querySelector(`.dropdown-item[data-sieve-authentication="${type}"]`) + .textContent; + + parent + .querySelector(".sieve-settings-authentication button") + .dataset.sieveAuthentication = type; + + parent + .querySelector(".sieve-settings-authentication button") + .textContent = text; + + return this; + } + + /** + * The authentication type which was selected + * + * @returns {string} + * authentication type + */ + getSaslMechanism() { + return this.getDialog() + .querySelector(".sieve-settings-authentication button") + .dataset.sieveAuthentication; + } + + /** + * Sets the username in the ui + * + * @param {string} username + * the username which should be set + * @returns {SieveSettingsUI} + * a self reference + */ + setAuthentication(username) { + this.getDialog() + .querySelector(".sieve-settings-username").value = username; + + return this; + } + + /** + * Gets the username from the ui. + * + * @returns {string} + * the username as string. + */ + getAuthentication() { + return this.getDialog() + .querySelector(".sieve-settings-username").value; + } + + /** + * Selects the given authorization type in the dropdown control. + * + * @param {int|string} type + * the authorization type to be activated. + * @returns {SieveCredentialsSettingsUI} + * a self reference. + */ + setAuthorizationType(type) { + const dialog = this.getDialog(); + + const text = dialog + .querySelector(".sieve-settings-authorization") + .querySelector(`.dropdown-item[data-sieve-authorization="${type}"]`) + .textContent; + + dialog + .querySelector(".sieve-settings-authorization button") + .dataset.sieveAuthorization = type; + + dialog + .querySelector(".sieve-settings-authorization button") + .textContent = text; + + if (`${type}` === "3") + dialog.querySelector(".sieve-settings-authorization-username").classList.remove("d-none"); + else + dialog.querySelector(".sieve-settings-authorization-username").classList.add("d-none"); + + return this; + } + + /** + * Gets the current authorization type set in the dropdown menu. + * + * @returns {string} + * the authorization type as string. + */ + getAuthorizationType() { + return this.getDialog() + .querySelector(".sieve-settings-authorization button") + .dataset.sieveAuthorization; + } + + /** + * Sets the authorization name. + * Authorization allows an authenticated user (normally admin) + * to access an other users sieve account. + * + * @param {string} username + * the username as which the current user should authorized + * @returns {SieveSettingsUI} + * a self reference + */ + setAuthorization(username) { + this.getDialog() + .querySelector(".sieve-settings-text-authorization-username").value = username; + + return this; + } + + /** + * Gets the authorization name from the ui. + * + * @returns {string} + * the authorized username. + */ + getAuthorization() { + return this.getDialog() + .querySelector(".sieve-settings-text-authorization-username").value; + } + + /** + * Gets the current dialogs encryption settings. + * + * @returns {boolean} + * true in case an encrypted connection should be used otherwise false. + */ + isEncrypted() { + + if (this.getDialog().querySelector("#sieve-settings-encryption-off").checked) + return false; + + return true; + } + + /** + * Sets the encryption settings in the current dialog. + * + * @param {boolean} encrypted + * the encryption status to set. False in case encryption is disabled + * otherwise it will be enabled + * @returns {SieveServerSettingsUI} + * a self reference + */ + setEncrypted(encrypted) { + const parent = this.getDialog(); + + if (encrypted === false) + parent.querySelector("#sieve-settings-encryption-off").checked = true; + else + parent.querySelector("#sieve-settings-encryption-on").checked = true; + + return this; + } + + /** + * Shows the advanced setting + */ + showAdvanced() { + const parent = this.getDialog(); + + parent.querySelector(".siv-settings-advanced").classList.remove("d-none"); + parent.querySelector(".siv-settings-show-advanced").classList.add("d-none"); + parent.querySelector(".siv-settings-hide-advanced").classList.remove("d-none"); + } + + /** + * Hides the advanced settings + */ + hideAdvanced() { + const parent = this.getDialog(); + + parent.querySelector(".siv-settings-advanced").classList.add("d-none"); + parent.querySelector(".siv-settings-show-advanced").classList.remove("d-none"); + parent.querySelector(".siv-settings-hide-advanced").classList.add("d-none"); + } + + /** + * Shows the settings dialog + * + * @returns {boolean} + * true in case new settings where applied. + * false in case the dialog was canceled. + */ + async show() { + + document.querySelector("#ctx").append( + await (new SieveTemplate()).load("./settings/ui/settings.credentials.html")); + + await this.render(); + + const dialog = this.getDialog(); + const modal = new bootstrap.Modal(dialog); + + modal.show(); + + dialog + .querySelector(".sieve-settings-apply") + .addEventListener("click", async () => { + await this.save(); + modal.hide(); + }); + + return await new Promise((resolve) => { + + dialog.addEventListener('hidden.bs.modal', () => { + modal.dispose(); + dialog.remove(); + + resolve(); + }); + + }); + } + + /** + * Validates and saves the setting before closing the dialog. + * In case the settings are invalid an error message is displayed. + */ + async save() { + + const settings = { + general: { + secure: this.isEncrypted(), + sasl: this.getSaslMechanism() + }, + authentication: { + username: this.getAuthentication(), + mechanism: 0 + }, + authorization: { + username: this.getAuthorization(), + mechanism: this.getAuthorizationType() + } + }; + + await this.account.send("account-settings-set-credentials", settings); + } + + /** + * Returns the currents dialogs UI Element. + * + * @returns {object} + * the dialogs UI elements. + */ + getDialog() { + return document.querySelector("#dialog-settings-credentials"); + } + + /** + * Renders the UI element into the dom. + */ + async render() { + const parent = this.getDialog(); + + const credentials = await this.account.send("account-setting-get-credentials"); + + // Authentication settings + this.setEncrypted(credentials.general.secure); + this.setSaslMechanism(credentials.general.sasl); + + this.setAuthentication(credentials.authentication.username); + + parent + .querySelectorAll(".sieve-settings-authentication .dropdown-item") + .forEach((item) => { + item.addEventListener("click", (event) => { + this.setSaslMechanism(event.target.dataset.sieveAuthentication); + }); + }); + + // Show the forget password button only when a password is stored. + if (credentials.authentication.stored) + parent.querySelector(".sieve-settings-forget-password").classList.remove("d-none"); + else + parent.querySelector(".sieve-settings-forget-password").classList.add("d-none"); + + parent + .querySelector(".sieve-settings-forget-password button") + .addEventListener("click", async () => { + await this.account.send("account-settings-forget-credentials"); + + this.getDialog() + .querySelector(".sieve-settings-forget-password").classList.add("d-none"); + }); + + // Authorization settings.... + this.setAuthorizationType(credentials.authorization.type); + this.setAuthorization(credentials.authorization.username); + + parent + .querySelectorAll(".sieve-settings-authorization .dropdown-item") + .forEach((item) => { + item.addEventListener("click", (event) => { + this.setAuthorizationType(event.target.dataset.sieveAuthorization); + }); + }); + + parent + .querySelector(".siv-settings-show-advanced") + .addEventListener("click", () => { this.showAdvanced(); }); + + parent + .querySelector(".siv-settings-hide-advanced") + .addEventListener("click", () => { this.hideAdvanced(); }); + + this.hideAdvanced(); + } +} + +export { SieveCredentialsSettingsUI }; diff --git a/src/app/libs/managesieve.ui/settings/ui/SieveServerSettingsUI.js b/src/app/libs/managesieve.ui/settings/ui/SieveServerSettingsUI.js deleted file mode 100644 index 08e3cc6a..00000000 --- a/src/app/libs/managesieve.ui/settings/ui/SieveServerSettingsUI.js +++ /dev/null @@ -1,287 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - /* global bootstrap */ - /* global SieveTemplate */ - - // eslint-disable-next-line no-magic-numbers - const ONE_MINUTE = 60 * 1000; - - /** - * A UI renderer for the sieve settings dialog - */ - class SieveServerSettingsUI { - - /** - * Initializes the settings - * @param {SieveAccount} account - * the account for which the settings edited. - */ - constructor(account) { - this.account = account; - } - - /** - * Sets the account's human readable display name - * @param {string} name - * the name which should be set. - * @returns {SieveServerSettingsUI} - * a self reference - */ - setDisplayName(name) { - this.getDialog() - .querySelector(".sieve-settings-displayname").value = name; - - return this; - } - - /** - * Gets the account's human readable display name - * @returns {string} - * the display name - */ - getDisplayName() { - return this.getDialog() - .querySelector(".sieve-settings-displayname").value; - } - - /** - * Sets the server's hostname. - * - * @param {string} hostname - * the hostname as string. - * @returns {SieveServerSettingsUI} - * a self reference - */ - setHostname(hostname) { - this.getDialog() - .querySelector(".sieve-settings-hostname").value = hostname; - - return this; - } - - /** - * Gets the server's hostname. - * - * @returns {string} - * the hostname - */ - getHostname() { - return this.getDialog() - .querySelector(".sieve-settings-hostname").value; - } - - /** - * Populates the server's port in the dialog - * - * @param {string} port - * the port - * @returns {SieveServerSettingsUI} - * a self reference - */ - setPort(port) { - this.getDialog() - .querySelector(".sieve-settings-port").value = port; - - return this; - } - - /** - * Gets the server's port. - * - * @returns {string} - * the port as string. - */ - getPort() { - return this.getDialog() - .querySelector(".sieve-settings-port").value; - } - - /** - * Sets the server's certificate fingerprint in the ui. - * The fingerprint is normally a sha checksum. - * - * @param {string} fingerprint - * the fingerprint. - * @returns {SieveServerSettingsUI} - * a self reference - */ - setFingerprint(fingerprint) { - this.getDialog() - .querySelector(".sieve-settings-fingerprint").value = fingerprint; - - return this; - } - - /** - * Gets the server's fingerprint from the setting ui. - * - * @returns {string} - * the certificate fingerprint - */ - getFingerprint() { - return this.getDialog() - .querySelector(".sieve-settings-fingerprint").value; - } - - - /** - * Sets the keep alive interval. - * - * @param {int} interval - * the keep alive interval in ms - * @returns {SieveServerSettingsUI} - * a self reference - */ - setKeepAlive(interval) { - // convert to seconds - interval = interval / ONE_MINUTE; - this.getDialog() - .querySelector(".sieve-settings-keepalive-interval").value = interval; - - return this; - } - - /** - * Gets the keep alive interval - * - * @returns {int} - * the keep alive interval - */ - getKeepAlive() { - const interval = this.getDialog() - .querySelector(".sieve-settings-keepalive-interval").value; - - return interval * ONE_MINUTE; - } - - /** - * Shows the advanced setting - * - */ - showAdvanced() { - const parent = this.getDialog(); - - parent.querySelector(".siv-settings-advanced").classList.remove("d-none"); - parent.querySelector(".siv-settings-show-advanced").classList.add("d-none"); - parent.querySelector(".siv-settings-hide-advanced").classList.remove("d-none"); - } - - /** - * Hides the advanced settings - * - */ - hideAdvanced() { - const parent = this.getDialog(); - - parent.querySelector(".siv-settings-advanced").classList.add("d-none"); - parent.querySelector(".siv-settings-show-advanced").classList.remove("d-none"); - parent.querySelector(".siv-settings-hide-advanced").classList.add("d-none"); - } - - - /** - * Renders the UI element into the dom. - */ - async render() { - const parent = this.getDialog(); - - const server = await this.account.send("account-get-server"); - - this.setDisplayName(server.displayName); - this.setHostname(server.hostname); - this.setPort(server.port); - this.setFingerprint(server.fingerprint); - - this.setKeepAlive(server.keepAlive); - - parent.querySelector(".siv-settings-show-advanced") - .addEventListener("click", () => { this.showAdvanced(); }); - parent.querySelector(".siv-settings-hide-advanced") - .addEventListener("click", () => { this.hideAdvanced(); }); - - this.hideAdvanced(); - } - - /** - * Shows the settings dialog - * @returns {Promise<boolean>} - * false in case the dialog was dismissed otherwise true. - */ - async show() { - - document.querySelector("#ctx").appendChild( - await (new SieveTemplate()).load("./settings/ui/settings.server.tpl")); - - await this.render(); - - const dialog = document.querySelector("#dialog-settings-server"); - const modal = new bootstrap.Modal(dialog); - - modal.show(); - - dialog - .querySelector(".sieve-settings-apply") - .addEventListener("click", async () => { - await this.save(); - modal.hide(); - }); - - return await new Promise((resolve) => { - - dialog.addEventListener('hidden.bs.modal', () => { - modal.dispose(); - dialog.parentNode.removeChild(dialog); - - resolve(); - }); - }); - } - - /** - * Validates and saves the setting before closing the dialog. - * In case the settings are invalid an error message is displayed. - * - */ - async save() { - - const server = { - displayName: await this.getDisplayName(), - hostname: await this.getHostname(), - port: await this.getPort(), - fingerprint: await this.getFingerprint(), - keepAlive: await this.getKeepAlive() - }; - - await this.account.send("account-set-server", server); - } - - /** - * Returns the currents dialogs UI Element. - * - * @returns {HTMLElement} - * the dialogs UI elements. - */ - getDialog() { - return document.querySelector("#dialog-settings-server"); - } - - } - if (typeof (module) !== "undefined" && module !== null && module.exports) - module.exports = SieveServerSettingsUI; - else - exports.SieveServerSettingsUI = SieveServerSettingsUI; - -})(this); diff --git a/src/app/libs/managesieve.ui/settings/ui/SieveServerSettingsUI.mjs b/src/app/libs/managesieve.ui/settings/ui/SieveServerSettingsUI.mjs new file mode 100644 index 00000000..53323dec --- /dev/null +++ b/src/app/libs/managesieve.ui/settings/ui/SieveServerSettingsUI.mjs @@ -0,0 +1,281 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + + +/* global bootstrap */ + +import { SieveTemplate } from "./../../utils/SieveTemplate.mjs"; + +// eslint-disable-next-line no-magic-numbers +const ONE_MINUTE = 60 * 1000; + +/** + * A UI renderer for the sieve settings dialog + */ +class SieveServerSettingsUI { + + /** + * Initializes the settings + * @param {SieveAccount} account + * the account for which the settings edited. + */ + constructor(account) { + this.account = account; + } + + /** + * Sets the account's human readable display name + * @param {string} name + * the name which should be set. + * @returns {SieveServerSettingsUI} + * a self reference + */ + setDisplayName(name) { + this.getDialog() + .querySelector(".sieve-settings-displayname").value = name; + + return this; + } + + /** + * Gets the account's human readable display name + * @returns {string} + * the display name + */ + getDisplayName() { + return this.getDialog() + .querySelector(".sieve-settings-displayname").value; + } + + /** + * Sets the server's hostname. + * + * @param {string} hostname + * the hostname as string. + * @returns {SieveServerSettingsUI} + * a self reference + */ + setHostname(hostname) { + this.getDialog() + .querySelector(".sieve-settings-hostname").value = hostname; + + return this; + } + + /** + * Gets the server's hostname. + * + * @returns {string} + * the hostname + */ + getHostname() { + return this.getDialog() + .querySelector(".sieve-settings-hostname").value; + } + + /** + * Populates the server's port in the dialog + * + * @param {string} port + * the port + * @returns {SieveServerSettingsUI} + * a self reference + */ + setPort(port) { + this.getDialog() + .querySelector(".sieve-settings-port").value = port; + + return this; + } + + /** + * Gets the server's port. + * + * @returns {string} + * the port as string. + */ + getPort() { + return this.getDialog() + .querySelector(".sieve-settings-port").value; + } + + /** + * Sets the server's certificate fingerprint in the ui. + * The fingerprint is normally a sha checksum. + * + * @param {string} fingerprint + * the fingerprint. + * @returns {SieveServerSettingsUI} + * a self reference + */ + setFingerprint(fingerprint) { + this.getDialog() + .querySelector(".sieve-settings-fingerprint").value = fingerprint; + + return this; + } + + /** + * Gets the server's fingerprint from the setting ui. + * + * @returns {string} + * the certificate fingerprint + */ + getFingerprint() { + return this.getDialog() + .querySelector(".sieve-settings-fingerprint").value; + } + + + /** + * Sets the keep alive interval. + * + * @param {int} interval + * the keep alive interval in ms + * @returns {SieveServerSettingsUI} + * a self reference + */ + setKeepAlive(interval) { + // convert to seconds + interval = interval / ONE_MINUTE; + this.getDialog() + .querySelector(".sieve-settings-keepalive-interval").value = interval; + + return this; + } + + /** + * Gets the keep alive interval + * + * @returns {int} + * the keep alive interval + */ + getKeepAlive() { + const interval = this.getDialog() + .querySelector(".sieve-settings-keepalive-interval").value; + + return interval * ONE_MINUTE; + } + + /** + * Shows the advanced setting + * + */ + showAdvanced() { + const parent = this.getDialog(); + + parent.querySelector(".siv-settings-advanced").classList.remove("d-none"); + parent.querySelector(".siv-settings-show-advanced").classList.add("d-none"); + parent.querySelector(".siv-settings-hide-advanced").classList.remove("d-none"); + } + + /** + * Hides the advanced settings + * + */ + hideAdvanced() { + const parent = this.getDialog(); + + parent.querySelector(".siv-settings-advanced").classList.add("d-none"); + parent.querySelector(".siv-settings-show-advanced").classList.remove("d-none"); + parent.querySelector(".siv-settings-hide-advanced").classList.add("d-none"); + } + + + /** + * Renders the UI element into the dom. + */ + async render() { + const parent = this.getDialog(); + + const server = await this.account.send("account-get-server"); + + this.setDisplayName(server.displayName); + this.setHostname(server.hostname); + this.setPort(server.port); + this.setFingerprint(server.fingerprint); + + this.setKeepAlive(server.keepAlive); + + parent.querySelector(".siv-settings-show-advanced") + .addEventListener("click", () => { this.showAdvanced(); }); + parent.querySelector(".siv-settings-hide-advanced") + .addEventListener("click", () => { this.hideAdvanced(); }); + + this.hideAdvanced(); + } + + /** + * Shows the settings dialog + * @returns {Promise<boolean>} + * false in case the dialog was dismissed otherwise true. + */ + async show() { + + document.querySelector("#ctx").append( + await (new SieveTemplate()).load("./settings/ui/settings.server.html")); + + await this.render(); + + const dialog = document.querySelector("#dialog-settings-server"); + const modal = new bootstrap.Modal(dialog); + + modal.show(); + + dialog + .querySelector(".sieve-settings-apply") + .addEventListener("click", async () => { + await this.save(); + modal.hide(); + }); + + return await new Promise((resolve) => { + + dialog.addEventListener('hidden.bs.modal', () => { + modal.dispose(); + dialog.remove(); + + resolve(); + }); + }); + } + + /** + * Validates and saves the setting before closing the dialog. + * In case the settings are invalid an error message is displayed. + * + */ + async save() { + + const server = { + displayName: await this.getDisplayName(), + hostname: await this.getHostname(), + port: await this.getPort(), + fingerprint: await this.getFingerprint(), + keepAlive: await this.getKeepAlive() + }; + + await this.account.send("account-set-server", server); + } + + /** + * Returns the currents dialogs UI Element. + * + * @returns {HTMLElement} + * the dialogs UI elements. + */ + getDialog() { + return document.querySelector("#dialog-settings-server"); + } + +} + +export { SieveServerSettingsUI }; diff --git a/src/app/libs/managesieve.ui/settings/ui/settings.credentials.tpl b/src/app/libs/managesieve.ui/settings/ui/settings.credentials.html index 37d37067..f3cd152a 100644 --- a/src/app/libs/managesieve.ui/settings/ui/settings.credentials.tpl +++ b/src/app/libs/managesieve.ui/settings/ui/settings.credentials.html @@ -4,7 +4,7 @@ <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" data-i18n="credentials.title"></h5> - <button type="button" class="btn-close" data-dismiss="modal" aria-label="Close"></button> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> <form> @@ -28,7 +28,7 @@ <label class="col-sm-3 col-form-label" data-i18n="credentials.authentication"></label> <div class="col-sm-8"> <div class="sieve-settings-authentication dropdown"> - <button class="btn btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown" + <button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> </button> <div class="dropdown-menu" aria-labelledby="dropdownMenuButton"> @@ -39,12 +39,12 @@ href="#"></a> <a class="dropdown-item" data-i18n="credentials.sasl.login" data-sieve-authentication="LOGIN" href="#"></a> - <a class="dropdown-item" data-i18n="credentials.sasl.crammd5" data-sieve-authentication="CRAM-MD5" - href="#"></a> <a class="dropdown-item" data-i18n="credentials.sasl.scramsha1" data-sieve-authentication="SCRAM-SHA-1" href="#"></a> <a class="dropdown-item" data-i18n="credentials.sasl.scramsha256" data-sieve-authentication="SCRAM-SHA-256" href="#"></a> + <a class="dropdown-item" data-i18n="credentials.sasl.scramsha512" + data-sieve-authentication="SCRAM-SHA-512" href="#"></a> <a class="dropdown-item" data-i18n="credentials.sasl.external" data-sieve-authentication="EXTERNAL" href="#"></a> <div class="dropdown-divider"></div> @@ -84,7 +84,7 @@ <div class="col-sm-8"> <div class="sieve-settings-authorization dropdown"> <button class="btn btn-outline-secondary dropdown-toggle" type="button" id="dropdownMenuButton" - data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> + data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> </button> <div class="dropdown-menu" aria-labelledby="dropdownMenuButton"> <a data-i18n="credentials.authorization.implicit" class="dropdown-item" data-sieve-authorization="0" diff --git a/src/app/libs/managesieve.ui/settings/ui/settings.server.tpl b/src/app/libs/managesieve.ui/settings/ui/settings.server.html index 1bd314d6..d36aec05 100644 --- a/src/app/libs/managesieve.ui/settings/ui/settings.server.tpl +++ b/src/app/libs/managesieve.ui/settings/ui/settings.server.html @@ -4,7 +4,7 @@ <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" data-i18n="server.title"></h5> - <button type="button" class="btn-close" data-dismiss="modal" aria-label="Close"></button> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> <form> diff --git a/src/app/libs/managesieve.ui/tabs/SieveTabsUI.js b/src/app/libs/managesieve.ui/tabs/SieveTabsUI.js deleted file mode 100644 index f6cf5509..00000000 --- a/src/app/libs/managesieve.ui/tabs/SieveTabsUI.js +++ /dev/null @@ -1,385 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - /* global bootstrap */ - const { SieveTemplate } = require("./../utils/SieveTemplate.js"); - const { SieveIpcClient } = require("./../utils/SieveIpcClient.js"); - const { SieveUniqueId } = require("./../utils/SieveUniqueId.js"); - - const SCROLL_INCREMENT = 100; - - /** - * Implements a single tab ui element. - */ - class SieveTab { - - /** - * Creates a new instance - * @param {SieveTabUI} tabs - * the parent tab ui object - * @param {string} account - * the accounts name - * @param {string} name - * the script'S name - */ - constructor(tabs, account, name) { - this.tabs = tabs; - this.account = account; - this.name = name; - } - - /** - * Gets the current tab. The dom element is used to store meta data. - * - * @returns {HTMLElement} - * the tab's dom element - */ - getTab() { - return document - .querySelector(`#tabs-items [data-sieve-account='${this.account}'][data-sieve-name='${this.name}']`); - } - - /** - * Gets the tab unique id. - * @returns {string} - * the unique tab id. - */ - getId() { - return this.getTab().dataset.sieveId; - } - - /** - * Ensures the tab's content is shown. - */ - show() { - const tab = this.getTab().querySelector(".nav-link"); - (new bootstrap.Tab(tab)).show(); - - // On Tab show is not fired when the tab is already visible. - // so we need to emulate this. In worst case we end up with a - // duplicated shown message... - this.onTabShown(); - } - - /** - * Returns the iframe associated with this tab - * - * @returns {IFrame} - * the iframe which hosts the content - */ - getContent() { - return document.querySelector(`#${this.getId()}-content`).contentWindow; - } - - /** - * Called whenever the tab got activated and the tab's content was - * shown to the user. - */ - onTabShown() { - SieveIpcClient.sendMessage("editor", "editor-shown", null, this.getContent()); - } - - /** - * Closes the tab and removes the tab content frame. - * It may be vetoed, e.g. if an editor contains unsaved changes. - * - * @returns {boolean} - * true in case the tab could be closed. In case it was canceled false. - */ - async close() { - - if (await this.hasChanges()) { - this.show(); - const rv = await SieveIpcClient.sendMessage("editor", "editor-close", this.name, this.getContent()); - - // Closing was canceled? - if (!rv) - return false; - } - - // we need to delete first the content... - const content = document.querySelector(`#${this.getId()}-content`); - content.parentNode.removeChild(content); - - // and then the tab, otherwise getId fails... - const tab = document.querySelector(`#${this.getId()}-tab`); - const elm = bootstrap.Tab.getInstance(tab); - - if (elm) - elm.dispose(); - - tab.parentNode.removeChild(tab); - - return true; - } - - /** - * Checks if the tab has unsaved changes. - * - * @returns {boolean} - * true in case of unsaved changes - */ - async hasChanges() { - return await SieveIpcClient.sendMessage("editor", "editor-hasChanged", null, this.getContent()); - } - - } - - /** - * Implements a tab ui. - */ - class SieveTabUI { - - /** - * Scrolls the tab bar to the right - */ - scrollRight() { - const lastTab = document.querySelector("#tabs-items").lastElementChild; - const tabs = document.querySelector("#tabs-items"); - - const lastTabRight = lastTab.offsetLeft + lastTab.offsetWidth; - - if (lastTabRight < tabs.clientWidth) { - tabs.style.left = `${0}px`; - return; - } - - let left = tabs.offsetLeft - SCROLL_INCREMENT; - - const max = tabs.offsetWidth - lastTabRight; - - if (left < max) - left = max; - - tabs.style.left = `${left}px`; - } - - /** - * Scrolls the tab bar to the left - */ - scrollLeft() { - const lastTab = document.querySelector("#tabs-items").lastElementChild; - const tabs = document.querySelector("#tabs-items"); - - const lastTabRight = lastTab.offsetLeft + lastTab.offsetWidth; - - - if (lastTabRight < tabs.clientWidth) { - tabs.style.left = `${0}px`; - return; - } - - let left = tabs.offsetLeft + SCROLL_INCREMENT; - - if (left > 0) - left = 0; - - tabs.style.left = `${left}px`; - } - - /** - * Initializes the TabUI - */ - init() { - - document - .querySelector("#tabs-items") - .style.transitionProperty = "left"; - - document - .querySelector("#tabs-items") - .style.transitionDuration = "0.5s"; - - // Add event listeners... - document - .querySelector("#tabs-scroll-left") - .addEventListener("click", () => { this.scrollLeft(); }); - - document - .querySelector("#tabs-scroll-right") - .addEventListener("click", () => { this.scrollRight(); }); - } - - /** - * Gets the tab by the account name and script name. - * - * @param {string} account - * the unique account name. - * @param {string} name - * the script name. - * - * @returns {SieveTab|null} - * the tab or null in case no such tab exists. - */ - getTab(account, name) { - const tab = new SieveTab(this, account, name); - - if (!tab.getTab()) - return null; - - return tab; - } - - /** - * Checks if a tab for the given script exists. - * - * @param {string} account - * the accounts unique id. - * @param {string} name - * the script name. - * - * @returns {boolean} - * true in case the tab exists otherwise false. - */ - has(account, name) { - return (this.getTab(account, name) !== null); - } - - /** - * Called whenever tab is shown when switching to it. - * - * @param {string} account - * the unique account id. - * @param {string} name - * the script name. - */ - onTabShown(account, name) { - this.getTab(account, name).onTabShown(); - } - - /** - * Closes the given tab. - * Tabs are identified by the unique account id plus the script name. - * @param {string} account - * the unique account id. - * @param {string} name - * the script name. - */ - async close(account, name) { - - const tab = this.getTab(account, name); - - if (!tab) - return; - - if (!await tab.close()) { - return; - } - - const newTab = document.querySelector("#accounts-tab .nav-link"); - (new bootstrap.Tab(newTab)).show(); - } - - /** - * Creates a unique id - * @returns {string} - * the unique id - */ - generateId() { - return (new SieveUniqueId()).generate(); - } - - /** - * Creates a new tab for the script. - * In case the tab exists it will switch to the tab. - * - * Tabs are identified by the account id and the script name. - * - * @param {string} account - * the unique account id - * @param {string} name - * the script name - */ - async create(account, name) { - - if (this.has(account, name)) { - this.open(account, name); - return; - } - - // Create a random id. - const id = this.generateId(); - - const tabId = `${id}-tab`; - const contentId = `${id}-content`; - - // create a new tab. - const content = await ( - new SieveTemplate()).load("./libs/managesieve.ui/tabs/editor.content.tpl"); - const tab = await ( - new SieveTemplate()).load("./libs/managesieve.ui/tabs/editor.tab.tpl"); - - tab.querySelector(".nav-link").href = `#${contentId}`; - tab.querySelector(".siv-tab-name").textContent = name; - - tab.querySelector(".tab-close").addEventListener("click", async () => { - await this.close(account, name); - }); - - content.id = contentId; - tab.id = tabId; - tab.dataset.sieveAccount = account; - tab.dataset.sieveName = name; - tab.dataset.sieveId = id; - - // Update the iframe's url. - const url = new URL(content.src, window.location); - - url.searchParams.append("account", account); - url.searchParams.append("script", name); - - content.src = url.toString(); - - document.querySelector(`#tabs-content`).appendChild(content); - document.querySelector(`#tabs-items`).appendChild(tab); - - tab.addEventListener('shown.bs.tab', () => { this.onTabShown(account, name); }); - - this.getTab(account, name).show(); - } - - /** - * Opens a script in a tab. - * - * In case the script is already open it will switch - * to this tab. - * - * Tabs are identified by the account id and the script name. - * - * @param {string} account - * the account id - * @param {string} name - * the script name - */ - async open(account, name) { - - const tab = this.getTab(account, name); - - if (tab) { - tab.show(); - return; - } - - await this.create(account, name); - } - - } - - if (typeof (module) !== "undefined" && module !== null && module.exports) - module.exports.SieveTabUI = SieveTabUI; - else - exports.SieveTabUI = SieveTabUI; - -})(this); diff --git a/src/app/libs/managesieve.ui/tabs/SieveTabsUI.mjs b/src/app/libs/managesieve.ui/tabs/SieveTabsUI.mjs new file mode 100644 index 00000000..dd163088 --- /dev/null +++ b/src/app/libs/managesieve.ui/tabs/SieveTabsUI.mjs @@ -0,0 +1,377 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +/* global bootstrap */ +import { SieveTemplate } from "./../utils/SieveTemplate.mjs"; +import { SieveIpcClient } from "./../utils/SieveIpcClient.mjs"; +import { SieveUniqueId } from "./../utils/SieveUniqueId.mjs"; + +const SCROLL_INCREMENT = 100; + +/** + * Implements a single tab ui element. + */ +class SieveTab { + + /** + * Creates a new instance + * @param {SieveTabUI} tabs + * the parent tab ui object + * @param {string} account + * the accounts name + * @param {string} name + * the script'S name + */ + constructor(tabs, account, name) { + this.tabs = tabs; + this.account = account; + this.name = name; + } + + /** + * Gets the current tab. The dom element is used to store meta data. + * + * @returns {HTMLElement} + * the tab's dom element + */ + getTab() { + return document + .querySelector(`#tabs-items [data-sieve-account='${this.account}'][data-sieve-name='${this.name}']`); + } + + /** + * Gets the tab unique id. + * @returns {string} + * the unique tab id. + */ + getId() { + return this.getTab().dataset.sieveId; + } + + /** + * Ensures the tab's content is shown. + */ + show() { + const tab = this.getTab().querySelector(".nav-link"); + (new bootstrap.Tab(tab)).show(); + + // On Tab show is not fired when the tab is already visible. + // so we need to emulate this. In worst case we end up with a + // duplicated shown message... + this.onTabShown(); + } + + /** + * Returns the iframe associated with this tab + * + * @returns {IFrame} + * the iframe which hosts the content + */ + getContent() { + return document.querySelector(`#${this.getId()}-content`).contentWindow; + } + + /** + * Called whenever the tab got activated and the tab's content was + * shown to the user. + */ + onTabShown() { + SieveIpcClient.sendMessage("editor", "editor-shown", null, this.getContent()); + } + + /** + * Closes the tab and removes the tab content frame. + * It may be vetoed, e.g. if an editor contains unsaved changes. + * + * @returns {boolean} + * true in case the tab could be closed. In case it was canceled false. + */ + async close() { + + if (await this.hasChanges()) { + this.show(); + const rv = await SieveIpcClient.sendMessage("editor", "editor-close", this.name, this.getContent()); + + // Closing was canceled? + if (!rv) + return false; + } + + // we need to delete first the content... + const content = document.querySelector(`#${this.getId()}-content`); + content.remove(); + + // and then the tab, otherwise getId fails... + const tab = document.querySelector(`#${this.getId()}-tab`); + const elm = bootstrap.Tab.getInstance(tab); + + if (elm) + elm.dispose(); + + tab.remove(); + + return true; + } + + /** + * Checks if the tab has unsaved changes. + * + * @returns {boolean} + * true in case of unsaved changes + */ + async hasChanges() { + return await SieveIpcClient.sendMessage("editor", "editor-hasChanged", null, this.getContent()); + } + +} + +/** + * Implements a tab ui. + */ +class SieveTabUI { + + /** + * Scrolls the tab bar to the right + */ + scrollRight() { + const lastTab = document.querySelector("#tabs-items").lastElementChild; + const tabs = document.querySelector("#tabs-items"); + + const lastTabRight = lastTab.offsetLeft + lastTab.offsetWidth; + + if (lastTabRight < tabs.clientWidth) { + tabs.style.left = `${0}px`; + return; + } + + let left = tabs.offsetLeft - SCROLL_INCREMENT; + + const max = tabs.offsetWidth - lastTabRight; + + if (left < max) + left = max; + + tabs.style.left = `${left}px`; + } + + /** + * Scrolls the tab bar to the left + */ + scrollLeft() { + const lastTab = document.querySelector("#tabs-items").lastElementChild; + const tabs = document.querySelector("#tabs-items"); + + const lastTabRight = lastTab.offsetLeft + lastTab.offsetWidth; + + + if (lastTabRight < tabs.clientWidth) { + tabs.style.left = `${0}px`; + return; + } + + let left = tabs.offsetLeft + SCROLL_INCREMENT; + + if (left > 0) + left = 0; + + tabs.style.left = `${left}px`; + } + + /** + * Initializes the TabUI + */ + init() { + + document + .querySelector("#tabs-items") + .style.transitionProperty = "left"; + + document + .querySelector("#tabs-items") + .style.transitionDuration = "0.5s"; + + // Add event listeners... + document + .querySelector("#tabs-scroll-left") + .addEventListener("click", () => { this.scrollLeft(); }); + + document + .querySelector("#tabs-scroll-right") + .addEventListener("click", () => { this.scrollRight(); }); + } + + /** + * Gets the tab by the account name and script name. + * + * @param {string} account + * the unique account name. + * @param {string} name + * the script name. + * + * @returns {SieveTab|null} + * the tab or null in case no such tab exists. + */ + getTab(account, name) { + const tab = new SieveTab(this, account, name); + + if (!tab.getTab()) + return null; + + return tab; + } + + /** + * Checks if a tab for the given script exists. + * + * @param {string} account + * the accounts unique id. + * @param {string} name + * the script name. + * + * @returns {boolean} + * true in case the tab exists otherwise false. + */ + has(account, name) { + return (this.getTab(account, name) !== null); + } + + /** + * Called whenever tab is shown when switching to it. + * + * @param {string} account + * the unique account id. + * @param {string} name + * the script name. + */ + onTabShown(account, name) { + this.getTab(account, name).onTabShown(); + } + + /** + * Closes the given tab. + * Tabs are identified by the unique account id plus the script name. + * @param {string} account + * the unique account id. + * @param {string} name + * the script name. + */ + async close(account, name) { + + const tab = this.getTab(account, name); + + if (!tab) + return; + + if (!await tab.close()) { + return; + } + + const newTab = document.querySelector("#accounts-tab .nav-link"); + (new bootstrap.Tab(newTab)).show(); + } + + /** + * Creates a unique id + * @returns {string} + * the unique id + */ + generateId() { + return (new SieveUniqueId()).generate(); + } + + /** + * Creates a new tab for the script. + * In case the tab exists it will switch to the tab. + * + * Tabs are identified by the account id and the script name. + * + * @param {string} account + * the unique account id + * @param {string} name + * the script name + */ + async create(account, name) { + + if (this.has(account, name)) { + this.open(account, name); + return; + } + + // Create a random id. + const id = this.generateId(); + + const tabId = `${id}-tab`; + const contentId = `${id}-content`; + + // create a new tab. + const content = await ( + new SieveTemplate()).load("./libs/managesieve.ui/tabs/editor.content.html"); + const tab = await ( + new SieveTemplate()).load("./libs/managesieve.ui/tabs/editor.tab.html"); + + tab.querySelector(".nav-link").href = `#${contentId}`; + tab.querySelector(".siv-tab-name").textContent = name; + + tab.querySelector(".tab-close").addEventListener("click", async () => { + await this.close(account, name); + }); + + content.id = contentId; + tab.id = tabId; + tab.dataset.sieveAccount = account; + tab.dataset.sieveName = name; + tab.dataset.sieveId = id; + + // Update the iframe's url. + const url = new URL(content.src, window.location); + + url.searchParams.append("account", account); + url.searchParams.append("script", name); + + content.src = url.toString(); + + document.querySelector(`#tabs-content`).append(content); + document.querySelector(`#tabs-items`).append(tab); + + tab.addEventListener('shown.bs.tab', () => { this.onTabShown(account, name); }); + + this.getTab(account, name).show(); + } + + /** + * Opens a script in a tab. + * + * In case the script is already open it will switch + * to this tab. + * + * Tabs are identified by the account id and the script name. + * + * @param {string} account + * the account id + * @param {string} name + * the script name + */ + async open(account, name) { + + const tab = this.getTab(account, name); + + if (tab) { + tab.show(); + return; + } + + await this.create(account, name); + } + +} + + +export { SieveTabUI }; diff --git a/src/app/libs/managesieve.ui/tabs/editor.content.tpl b/src/app/libs/managesieve.ui/tabs/editor.content.html index a35ccabf..a35ccabf 100644 --- a/src/app/libs/managesieve.ui/tabs/editor.content.tpl +++ b/src/app/libs/managesieve.ui/tabs/editor.content.html diff --git a/src/app/libs/managesieve.ui/tabs/editor.tab.tpl b/src/app/libs/managesieve.ui/tabs/editor.tab.html index c1ad9acf..5aeb14cc 100644 --- a/src/app/libs/managesieve.ui/tabs/editor.tab.tpl +++ b/src/app/libs/managesieve.ui/tabs/editor.tab.html @@ -1,5 +1,5 @@ <li class="nav-item"> - <a class="nav-link" id="profile-tab" data-toggle="tab" role="tab"> + <a class="nav-link" id="profile-tab" data-bs-toggle="tab" role="tab"> <span class="siv-tab-name">NAME</span> <span class="tab-close" style="padding-left: 5px">✕</span> </a> diff --git a/src/app/libs/managesieve.ui/updater/SieveUpdater.js b/src/app/libs/managesieve.ui/updater/SieveUpdater.js deleted file mode 100644 index 519fe3bf..00000000 --- a/src/app/libs/managesieve.ui/updater/SieveUpdater.js +++ /dev/null @@ -1,178 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - const SIEVE_GITHUB_UPDATE_URL = "https://thsmi.github.io/sieve/update.json"; - const MAJOR_VERSION = 0; - const MINOR_VERSION = 1; - const PATCH_VERSION = 2; - - /** - * Checks for Updates on github. - */ - class SieveUpdater { - - /** - * Converts the given string into an integer - * @param {string} version - * the version number as string which should be converted to integer. - * @returns {Integer|NaN} - * returns the integer value or NaN in case the string is no integer. - */ - getInt(version) { - const value = parseInt(version, 10); - - if (Number.isInteger(value)) - return value; - - return Number.NaN; - } - - - /** - * Checks if the current version is less than the new version. - * - * For comparison the string values are converted to an integer. - * In case no integer comparison is possible a string comparison will be performed. - * - * @param {string} newVersion - * the new version as string - * @param {string} currentVersion - * the current version as string - * - * @returns {boolean} - * false in case the current version is the latest - * true in case there is a newer version - */ - isLessThan(newVersion, currentVersion) { - - const newValue = this.getInt(newVersion); - const currentValue = this.getInt(currentVersion); - - // in case conversion failed we use string comparison - if (newValue === Number.NaN || currentValue === Number.NaN) - return (newVersion < currentVersion); - - return newValue < currentValue; - } - - /** - * Checks if the current version is greater than the new version. - * - * For comparison the string values are converted to an integer. - * In case no integer comparison is possible a string comparison will be performed. - * - * @param {string} newVersion - * the new version as string - * @param {string} currentVersion - * the current version as string - * - * @returns {boolean} - * true in case the new version is larger than the current - * false in the new version is smaller than the current. - */ - isGreaterThan(newVersion, currentVersion) { - const newValue = this.getInt(newVersion); - const currentValue = this.getInt(currentVersion); - - // in case conversion failed we use string comparison - if (newValue === Number.NaN || currentValue === Number.NaN) - return (newVersion > currentVersion); - - return newValue > currentValue; - } - - /** - * Compares if the next version is older than the current version. - * - * @param {string} next - * the next version as dot separated string. - * @param {string} current - * the current version as dot separated string. - * @returns {boolean} - * true in case the current version is older than the next version otherwise false. - */ - isOlder(next, current) { - current = current.split("."); - next = next.split("."); - - // In case the new major is larger, then this version is definitely older. - if (this.isGreaterThan(next[MAJOR_VERSION], current[MAJOR_VERSION])) - return false; - // In case the new major is smaller, then this version is definitely newer. - if (this.isLessThan(next[MAJOR_VERSION], current[MAJOR_VERSION])) - return true; - - // In case it is equal we need to check at the minor version - // In case the new minor is larger, then this version is definitely older. - if (this.isGreaterThan(next[MINOR_VERSION], current[MINOR_VERSION])) - return false; - // In case the new minor is smaller, then this version is definitely newer. - if (this.isLessThan(next[MINOR_VERSION], current[MINOR_VERSION])) - return true; - - // In case it is equal we need to check the patch level. - // It is newer if it is larger - if (this.isGreaterThan(next[PATCH_VERSION], current[PATCH_VERSION])) - return false; - - // Otherwise in case it is less or equal, the version is older or the same. - return true; - } - - /** - * Compares the current version against the manifest. - * @param {object} manifest - * the manifest with the version information - * @param {string} currentVersion - * the apps current version. - * @returns {boolean} - * false if the current version is the latest. - * true in case the manifest contains a newer version definition. - */ - compare(manifest, currentVersion) { - const items = manifest["addons"]["sieve@mozdev.org"]["updates"]; - - // There are no updates if all entries are less or equal to the current version - for (const item of items) { - - if (this.isOlder(item.version, currentVersion)) - continue; - - return true; - } - - return false; - } - - /** - * Checks the if any updates are published at github. - * @returns {boolean} - * true if newer version are available, otherwise false. - */ - async check() { - - const currentVersion = await (require('electron').ipcRenderer.invoke("get-version")); - - return this.compare( - await (await fetch(SIEVE_GITHUB_UPDATE_URL)).json(), - currentVersion); - } - } - - - if (typeof (module) !== "undefined" && module && module.exports) - module.exports.SieveUpdater = SieveUpdater; - else - exports.SieveUpdater = SieveUpdater; - -})(this); diff --git a/src/app/libs/managesieve.ui/updater/SieveUpdater.mjs b/src/app/libs/managesieve.ui/updater/SieveUpdater.mjs new file mode 100644 index 00000000..13fba281 --- /dev/null +++ b/src/app/libs/managesieve.ui/updater/SieveUpdater.mjs @@ -0,0 +1,171 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + + +const SIEVE_GITHUB_UPDATE_URL = "https://thsmi.github.io/sieve/update.json"; +const MAJOR_VERSION = 0; +const MINOR_VERSION = 1; +const PATCH_VERSION = 2; + +/** + * Checks for Updates on github. + */ +class SieveUpdater { + + /** + * Converts the given string into an integer + * @param {string} version + * the version number as string which should be converted to integer. + * @returns {Integer|NaN} + * returns the integer value or NaN in case the string is no integer. + */ + getInt(version) { + const value = Number.parseInt(version, 10); + + if (Number.isInteger(value)) + return value; + + return Number.NaN; + } + + + /** + * Checks if the current version is less than the new version. + * + * For comparison the string values are converted to an integer. + * In case no integer comparison is possible a string comparison will be performed. + * + * @param {string} newVersion + * the new version as string + * @param {string} currentVersion + * the current version as string + * + * @returns {boolean} + * false in case the current version is the latest + * true in case there is a newer version + */ + isLessThan(newVersion, currentVersion) { + + const newValue = this.getInt(newVersion); + const currentValue = this.getInt(currentVersion); + + // in case conversion failed we use string comparison + if (newValue === Number.NaN || currentValue === Number.NaN) + return (newVersion < currentVersion); + + return newValue < currentValue; + } + + /** + * Checks if the current version is greater than the new version. + * + * For comparison the string values are converted to an integer. + * In case no integer comparison is possible a string comparison will be performed. + * + * @param {string} newVersion + * the new version as string + * @param {string} currentVersion + * the current version as string + * + * @returns {boolean} + * true in case the new version is larger than the current + * false in the new version is smaller than the current. + */ + isGreaterThan(newVersion, currentVersion) { + const newValue = this.getInt(newVersion); + const currentValue = this.getInt(currentVersion); + + // in case conversion failed we use string comparison + if (newValue === Number.NaN || currentValue === Number.NaN) + return (newVersion > currentVersion); + + return newValue > currentValue; + } + + /** + * Compares if the next version is older than the current version. + * + * @param {string} next + * the next version as dot separated string. + * @param {string} current + * the current version as dot separated string. + * @returns {boolean} + * true in case the current version is older than the next version otherwise false. + */ + isOlder(next, current) { + current = current.split("."); + next = next.split("."); + + // In case the new major is larger, then this version is definitely older. + if (this.isGreaterThan(next[MAJOR_VERSION], current[MAJOR_VERSION])) + return false; + // In case the new major is smaller, then this version is definitely newer. + if (this.isLessThan(next[MAJOR_VERSION], current[MAJOR_VERSION])) + return true; + + // In case it is equal we need to check at the minor version + // In case the new minor is larger, then this version is definitely older. + if (this.isGreaterThan(next[MINOR_VERSION], current[MINOR_VERSION])) + return false; + // In case the new minor is smaller, then this version is definitely newer. + if (this.isLessThan(next[MINOR_VERSION], current[MINOR_VERSION])) + return true; + + // In case it is equal we need to check the patch level. + // It is newer if it is larger + if (this.isGreaterThan(next[PATCH_VERSION], current[PATCH_VERSION])) + return false; + + // Otherwise in case it is less or equal, the version is older or the same. + return true; + } + + /** + * Compares the current version against the manifest. + * @param {object} manifest + * the manifest with the version information + * @param {string} currentVersion + * the apps current version. + * @returns {boolean} + * false if the current version is the latest. + * true in case the manifest contains a newer version definition. + */ + compare(manifest, currentVersion) { + const items = manifest["addons"]["sieve@mozdev.org"]["updates"]; + + // There are no updates if all entries are less or equal to the current version + for (const item of items) { + + if (this.isOlder(item.version, currentVersion)) + continue; + + return true; + } + + return false; + } + + /** + * Checks the if any updates are published at github. + * @returns {boolean} + * true if newer version are available, otherwise false. + */ + async check() { + + const currentVersion = await (require('electron').ipcRenderer.invoke("get-version")); + + return this.compare( + await (await fetch(SIEVE_GITHUB_UPDATE_URL)).json(), + currentVersion); + } +} + +export { SieveUpdater }; diff --git a/src/app/libs/managesieve.ui/updater/SieveUpdaterUI.js b/src/app/libs/managesieve.ui/updater/SieveUpdaterUI.js deleted file mode 100644 index 47620065..00000000 --- a/src/app/libs/managesieve.ui/updater/SieveUpdaterUI.js +++ /dev/null @@ -1,48 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - /* global SieveTemplate */ - /* global SieveIpcClient */ - - /** - * Checks for new updates and display a new message if a newer version is available - **/ - class SieveUpdaterUI { - - /** - * Checks for new updates and display a new message if a newer version is available - */ - async check() { - const status = await SieveIpcClient.sendMessage("core", "update-check"); - - if (status !== true) - return; - - const template = await (new SieveTemplate()).load("./updater/update.tpl"); - template.querySelector(".sieve-update-msg").addEventListener("click", () => { - SieveIpcClient.sendMessage("core", "update-goto-url"); - }); - - const parent = document.querySelector("#ctx"); - parent.insertBefore(template, parent.firstChild); - } - } - - if (typeof (module) !== "undefined" && module !== null && module.exports) - module.exports = SieveUpdaterUI; - else - exports.SieveUpdaterUI = SieveUpdaterUI; - -})(this); diff --git a/src/app/libs/managesieve.ui/updater/SieveUpdaterUI.mjs b/src/app/libs/managesieve.ui/updater/SieveUpdaterUI.mjs new file mode 100644 index 00000000..5c2c459e --- /dev/null +++ b/src/app/libs/managesieve.ui/updater/SieveUpdaterUI.mjs @@ -0,0 +1,39 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +import { SieveTemplate } from "./../utils/SieveTemplate.mjs"; +import { SieveIpcClient } from "./../utils/SieveIpcClient.mjs"; + +/** + * Checks for new updates and display a new message if a newer version is available + **/ +class SieveUpdaterUI { + + /** + * Checks for new updates and display a new message if a newer version is available + */ + async check() { + const status = await SieveIpcClient.sendMessage("core", "update-check"); + + if (status !== true) + return; + + const template = await (new SieveTemplate()).load("./updater/update.html"); + template.querySelector(".sieve-update-msg").addEventListener("click", () => { + SieveIpcClient.sendMessage("core", "update-goto-url"); + }); + + const parent = document.querySelector("#ctx"); + parent.insertBefore(template, parent.firstChild); + } +} + +export { SieveUpdaterUI }; diff --git a/src/app/libs/managesieve.ui/updater/update.tpl b/src/app/libs/managesieve.ui/updater/update.html index 3e184b24..1cb98c92 100644 --- a/src/app/libs/managesieve.ui/updater/update.tpl +++ b/src/app/libs/managesieve.ui/updater/update.html @@ -1,5 +1,5 @@ <div class="alert alert-info alert-dismissible fade show mt-4" role="alert"> <strong data-i18n="updater.title"></strong> <span class="sieve-update-msg" style="cursor: pointer" data-i18n="updater.message"></span> - <button type="button" class="btn-close" data-dismiss="modal" aria-label="Close"></button> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div>
\ No newline at end of file diff --git a/src/app/libs/managesieve.ui/utils/SieveIpcClient.js b/src/app/libs/managesieve.ui/utils/SieveIpcClient.js deleted file mode 100644 index ca0e07ab..00000000 --- a/src/app/libs/managesieve.ui/utils/SieveIpcClient.js +++ /dev/null @@ -1,83 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - const { SieveLogger } = require("./SieveLogger.js"); - const { SieveAbstractIpcClient } = require("./SieveAbstractIpcClient.js"); - - /** - * Implements a IPC based on the postMessage interface. - */ - class SieveIpcClient extends SieveAbstractIpcClient { - - /** - * @inheritdoc - */ - static getLogger() { - return SieveLogger.getInstance(); - } - - /** - * @inheritdoc - */ - static parseMessageFromEvent(e) { - return JSON.parse(e.data); - } - - /** - * @inheritdoc - */ - static dispatch(message, target, origin) { - if (origin === undefined) - origin = "*"; - - if (target === undefined) - target = parent; - - if (typeof (message) !== 'string') { - message = JSON.stringify(message); - } - - this.getLogger().logIpcMessage(`Sending message ${message}`); - - if (target !== window) { - target.postMessage(message, origin); - return; - } - - for (let idx = 0; idx < frames.length; idx++) - frames[idx].postMessage(message, origin); - } - - /** - * @inheritdoc - */ - static onMessage(e) { - if (e.source === window) - return; - - super.onMessage(e); - } - } - - window.addEventListener("message", (ev) => { SieveIpcClient.onMessage(ev); }, false); - - // Require modules need to use export.module - if (typeof (module) !== "undefined" && module && module.exports) - module.exports.SieveIpcClient = SieveIpcClient; - else - exports.SieveIpcClient = SieveIpcClient; - -})(this); - diff --git a/src/app/libs/managesieve.ui/utils/SieveIpcClient.mjs b/src/app/libs/managesieve.ui/utils/SieveIpcClient.mjs new file mode 100644 index 00000000..efa2f850 --- /dev/null +++ b/src/app/libs/managesieve.ui/utils/SieveIpcClient.mjs @@ -0,0 +1,66 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +import { SieveLogger } from "./SieveLogger.mjs"; +import { SieveAbstractIpcClient } from "./SieveAbstractIpcClient.mjs"; + +/** + * Implements a IPC based on the postMessage interface. + */ +class SieveIpcClient extends SieveAbstractIpcClient { + + /** + * @inheritdoc + */ + static getLogger() { + return SieveLogger.getInstance(); + } + + /** + * @inheritdoc + */ + static parseMessageFromEvent(e) { + return JSON.parse(e.data); + } + + /** + * @inheritdoc + */ + static dispatch(message, target, origin) { + if (origin === undefined) + origin = "*"; + + if (target === undefined) + target = parent; + + if (typeof (message) !== 'string') { + message = JSON.stringify(message); + } + + this.getLogger().logIpcMessage(`Sending message ${message}`); + + target.postMessage(message, origin); + + // In case the target is the current window, we also notify + // all child frames. + if (target !== window) + return; + + for (let idx = 0; idx < frames.length; idx++) + frames[idx].postMessage(message, origin); + } + +} + +window.addEventListener("message", (ev) => { SieveIpcClient.onMessage(ev); }, false); + +export { SieveIpcClient }; + diff --git a/src/app/main_esm.js b/src/app/main_esm.js new file mode 100644 index 00000000..111912c9 --- /dev/null +++ b/src/app/main_esm.js @@ -0,0 +1,21 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +(async () => { + const electron = await require('electron'); + + try { + await (await import('./sieve.mjs')).main(electron); + } catch (ex) { + // eslint-disable-next-line no-console + console.log(ex); + } +})(); diff --git a/src/app/sieve.mjs b/src/app/sieve.mjs new file mode 100644 index 00000000..49ae6b62 --- /dev/null +++ b/src/app/sieve.mjs @@ -0,0 +1,185 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +import path from 'path'; +import url from 'url'; + +const DEFAULT_WINDOW_WIDTH = 1200; +const DEFAULT_WINDOW_HEIGHT = 600; + +// Out main window, it defines the lifecycle of your application +// so we need to protect it from the garbage collector and keep a +// global reference active. Otherwise the window will be automatically +// cleaned up and thus closed. + +let win = null; + +/** + * Creates the main window + * + * @param {Electron} electron + * a reference to the electron's root context object. + */ +function createWindow(electron) { + + const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); + + let icon = undefined; + if (process.platform === "linux") + icon = path.join(__dirname, 'libs/icons/linux.png'); + + // Create the browser window. + win = new electron.BrowserWindow({ + width: DEFAULT_WINDOW_WIDTH, + height: DEFAULT_WINDOW_HEIGHT, + icon: icon, + webPreferences: { + // nodeIntegrationInSubFrames: true, + nodeIntegration: true, + contextIsolation: false + } + }); + + // Hide the menu bar. + win.removeMenu(); + + // and load the index.html of the app. + win.loadURL(url.format({ + pathname: path.join(__dirname, 'app.html'), + protocol: 'file:', + slashes: true + })); + + // Open the DevTools. + win.webContents.openDevTools(); + + // Emitted when the window is closed. + win.on('closed', () => { + // Dereference the window object, usually you would store windows + // in an array if your app supports multi windows, this is the time + // when you should delete the corresponding element. + win = null; + }); + + // As suggested in https://github.com/electron/electron/issues/4068 + const inputMenu = electron.Menu.buildFromTemplate([ + { role: 'cut' }, + { role: 'copy' }, + { role: 'paste' } + ]); + + win.webContents.on('context-menu', (e, props) => { + const { isEditable } = props; + if (isEditable) { + inputMenu.popup(win); + } + }); + + const handleRedirect = (e, uri) => { + if (uri !== win.webContents.getURL()) { + e.preventDefault(); + electron.shell.openExternal(uri); + } + }; + + // win.webContents.on('will-navigate', handleRedirect); + win.webContents.on('new-window', handleRedirect); +} + +/** + * The main entry point into this application. + * + * @param {Electron} electron + * a reference to the electron's root context object. + */ +async function main(electron) { + + // ensure we are running as a singleton. + const isLocked = electron.app.requestSingleInstanceLock(); + + if (!isLocked) { + // eslint-disable-next-line no-console + console.log("Exiting app is locked"); + electron.app.quit(); + return; + } + + electron.app.on('second-instance', () => { + // Someone tried to run a second instance, we should focus our window. + if (!win) + return; + + if (win.isMinimized()); + win.restore(); + + win.focus(); + }); + + electron.ipcMain.handle("open-dialog", async(event, options) => { + return await electron.dialog.showOpenDialog(options); + }); + + electron.ipcMain.handle("save-dialog", async(event, options) => { + return await electron.dialog.showSaveDialog(options); + }); + + electron.ipcMain.handle("get-version", async() => { + return await electron.app.getVersion(); + }); + + electron.ipcMain.handle("open-developer-tools", () => { + // Open the DevTools. + win.webContents.openDevTools({ + "mode": "detach", + "activate" : true + }); + }); + + electron.ipcMain.handle("reload-ui", () => { + // Force reload... + win.webContents.reloadIgnoringCache(); + }); + + + await electron.app.whenReady(); + + createWindow(electron); + + // This method will be called when Electron has finished + // initialization and is ready to create browser windows. + // Some APIs can only be used after this event occurs. + electron.app.on('ready', createWindow); + + // Quit when all windows are closed. + electron.app.on('window-all-closed', () => { + // On macOS it is common for applications and their menu bar + // to stay active until the user quits explicitly with Cmd + Q + if (process.platform !== 'darwin') { + electron.app.quit(); + } + }); + + electron.app.on('activate', () => { + // On macOS it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (win === null) { + createWindow(electron); + } + }); + + // In this file you can include the rest of your app's specific main process + // code. You can also put them in separate files and require them here. + +} + +export { + main +}; diff --git a/src/app/utils/SievePasswordManager.js b/src/app/utils/SievePasswordManager.js deleted file mode 100755 index 07793509..00000000 --- a/src/app/utils/SievePasswordManager.js +++ /dev/null @@ -1,145 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - const crypto = require('crypto'); - - /** - * Implements a very simplictic password manager. - * Which uses AES encryption to protect the saved passwords - */ - class SievePasswordManager { - - /** - * Initializes the password manager. - * - * @param {Function} callback - * a callback function which implements a password prompt - */ - constructor(callback) { - this.algorithm = 'aes-256-ctr'; - this.master = null; - this.callback = callback; - this.data = {}; - } - - /** - * Locks the password manager. - * This means it drops the cached master password. - * - * - */ - lock() { - this.master = null; - } - - /** - * Unlocks the password manager. - * In case it the password manager is locked the callback is invoked - * in order to retieve the master password. - * - * - */ - async unlock() { - if (this.isLocked()) - this.master = await this.callback(); - } - - /** - * Checks if the password manager is locked. - * Locked means the user has not provided the master password. - * - * @returns {boolean} - * true in case the passwor managerd is locked otherwise false - */ - isLocked() { - return this.master === null; - } - - /** - * Encrypts the given text - * @param {string} text - * the plain text - * @returns {string} - * the encrypted text as hex string - */ - encrypt(text) { - const cipher = crypto.createCipher(this.algorithm, this.master); - let crypted = cipher.update(text, 'utf8', 'hex'); - crypted += cipher.final('hex'); - - return crypted; - } - - /** - * Decrypts the given text - * @param {string} text - * the encrypted text as hex string - * @returns {string} - * the plain text - */ - decrypt(text) { - const decipher = crypto.createDecipher(this.algorithm, this.master); - let dec = decipher.update(text, 'hex', 'utf8'); - dec += decipher.final('utf8'); - - return dec; - } - - /** - * Adds or updates a password entry - * @param {string} id - * the unique id, which is used to identify the password. - * Typically the account id. - * @param {string} password - * the password which should be stored. - * - */ - set(id, password) { - this.unlock(); - this.data[id] = this.encrypt(password); - } - - /** - * Gets and decrypts a password from the the password store - * @param {string} id - * the unique id, which is used to identify the password. - * Typically the account id. - * @returns {Promise<string>} - * the decrypted string. - */ - get(id) { - this.unlock(); - return this.decrypt(this.data[id]); - } - - /** - * Removes the password - * @param {string} id - * the unique id, which is used to identify the password. - * Typically the account id. - * - */ - forget(id) { - delete this.data[id]; - } - } - - // Require modules need to use export.module - if (typeof(module) !== "undefined" && module && module.exports) - module.exports.SievePasswordManager = SievePasswordManager; - else - exports.SievePasswordManager = SievePasswordManager; - -})(this); diff --git a/src/common/libManageSieve/SieveAbstractBase64.mjs b/src/common/libManageSieve/SieveAbstractBase64.mjs new file mode 100644 index 00000000..039b7d55 --- /dev/null +++ b/src/common/libManageSieve/SieveAbstractBase64.mjs @@ -0,0 +1,96 @@ +/* +* The content of this file is licensed. You may obtain a copy of +* the license at https://github.com/thsmi/sieve/ or request it via +* email from the author. +* +* Do not remove or change this comment. +* +* The initial author of the code is: +* Thomas Schmid <schmid-thomas@gmx.net> +*/ + +/** + * Implements a binary base64 encoder. + * We can not use btoa as is does not support UTF-8 + */ +class SieveAbstractBase64Encoder { + + /** + * Creates a new Encoder instance. + * + * @param {Uint8Array|string} decoded + * the data to be encoded. Either an byte array or an UTF8 String + */ + constructor(decoded) { + this.decoded = decoded; + } + + /** + * Encodes the data into base 64 and returns a byte array. + * + * @returns {Uint8Array} + * the encoded data as byte array + * + * @abstract + */ + toArray() { + throw new Error("Implement SieveAbstractBase64Encoder"); + } + + /** + * Encodes the data into base64 and returns a string. + * + * @returns {string} + * the encoded data as string + * + * @abstract + */ + toUtf8() { + throw new Error("Implement SieveAbstractBase64Encoder"); + } +} + +/** + * Implements a binary base64 decoded, due to historic reasons + * browser only support decoding UTF-16 and not UTF-8. + * + * Node uses a Buffer for decoding. + */ +class SieveAbstractBase64Decoder { + /** + * Creates a new decoder instance + * + * @param {string | Uint8Array} encoded + * the encoded string which should be decoded + */ + constructor(encoded) { + this.encoded = encoded; + } + + /** + * Decodes the base64 data into a raw array + * + * @returns {Uint8Array} + * the output as raw array. + * + * @abstract + */ + toArray() { + throw new Error(`Implement SieveAbstractBase64Decoder::raw()`); + } + + /** + * Decodes the base64 array into a UTF8 String + * + * @returns {string} + * the decoded array as UTF8 string + */ + toUtf8() { + return (new TextDecoder("UTF-8")).decode(this.toArray()); + } +} + +export { + SieveAbstractBase64Decoder, + SieveAbstractBase64Encoder +}; diff --git a/src/common/libManageSieve/SieveAbstractClient.js b/src/common/libManageSieve/SieveAbstractClient.js deleted file mode 100755 index 8d793f25..00000000 --- a/src/common/libManageSieve/SieveAbstractClient.js +++ /dev/null @@ -1,695 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -/* - * This class is a simple socket implementation for the manage sieve protocol. - * Due to the asymmetric nature of the Mozilla sockets we need message queue. - * <p> - * New requests are added via the "addRequest" method. In case of a response, - * the corresponding request will be automatically called back via its - * "addResponse" method. - * <p> - * If you need a secure connection, set the flag secure in the constructor. - * Then connect to the host. And invoke the "startTLS" Method as soon as you - * negotiated the switch to a encrypted connection. After calling startTLS - * Mozilla will immediately switch to an encrypted connection. - * <p> - */ - -(function (exports) { - - "use strict"; - - const DEFAULT_TIMEOUT = 20000; - const NO_IDLE = 0; - - /** - * An abstract implementation for the manage sieve protocol. - * - * It implements a message pump and parsing facility. - * Only the connections to the transport are needed to be implemented. - * - * The javascript syntax for this code is extremely limited. - * As this code is used for a mozilla module as well as in node js. - * - * Due to various limitation there is no window object and also no toSource(). - * Same applies to timeouts. They need to be implemented with platform - * specific code. - * - * In general you should avoid the "new" operator as this makes imports difficult. - * Mozilla's Modules, Node's Require and the new ES imports are mostly - * incompatible to each other. - * - */ - class SieveAbstractClient { - - /** - * Creates a new instance - */ - constructor() { - this.host = null; - this.port = null; - - this.socket = null; - this.data = null; - - this.queueLocked = false; - - this.requests = []; - - this.idleDelay = 0; - - - // out of the box we support the following manage sieve commands... - // ... the server might advertise additional commands they are added ... - // ... or removed by the set compatibility method - this.compatibility = { - authenticate: true, - starttls: true, - logout: true, - capability: true, - // until now we do not support havespace... - // havespace : false, - putscript: true, - listscripts: true, - setactive: true, - getscript: true, - deletescript: true - }; - } - - /** - * Gives this socket a hint, whether a sieve commands is supported or not. - * - * Setting the corresponding attribute to false, indicates, that a sieve command - * should not be used. As this is only an advice, such command will still be - * processed by this sieve socket. - * - * By default the socket seek maximal compatibility. - * - * @param {object} capabilities commands - * the supported sieve commands as an associative array. Attribute names have - * to be in lower case, the values can be either null, undefined, true or false. - * - * @example - * sieve.setCompatibility({checkscript:true, rename:true, starttls:false}); - */ - setCompatibility(capabilities) { - for (const capability in capabilities) - this.compatibility[capability] = capabilities[capability]; - } - - /** - * Returns a list of supported sieve commands. As the socket seeks - * maximal compatibility, it always suggest the absolute minimal sieve - * command set defined in the rfc. This value is only a hint, and does - * not represent the server's capabilities! - * - * A command is most likely unsupported if the corresponding attribute is null and - * disabled if the the attribute is false - * - * You should override these defaults as soon as possible. - * - * @returns {Struct} - * an associative array structure indicating supported sieve command. - * Unsupported commands are indicated by a null, disabled by false value... - * - * @example - * if (sieve.getCompatibility().putscript) { - * // put script command supported... - * } - */ - getCompatibility() { - return this.compatibility; - } - - - /** - * Gets a reference to the current logger - * @returns {SieveAbstractLogger} - * the current logger - * - * @abstract - */ - getLogger() { - throw new Error("Implement getLogger()"); - } - - /** - * Checks if the connection to the server is still alive and can be used to send - * and receive messages - * @returns {boolean} - * true in case the connection is alive otherwise false - */ - isAlive() { - if (!this.socket) - return false; - - return true; - } - - /** - * Check is the connection supports any connection security. - * It could be either disabled by the client or the server. - * - * @abstract - * - * @returns {boolean} - * true in case the connection can be or is secure otherwise false - */ - isSecure() { - throw new Error("Implement isSecure()"); - } - - /** - * This method secures the connection to the sieve server. By activating - * Transport Layer Security all Data exchanged is encrypted. - * - * Before calling this method you need to request a encrypted connection by - * sending a startTLSRequest. Invoke this method immediately after the server - * confirms switching to TLS. - * - * @returns {SieveAbstractClient} - * a self reference - **/ - startTLS() { - if (!this.isSecure()) - throw new Error("TLS can't be started no secure socket"); - - if (!this.socket) - throw new Error(`Can't start TLS, your are not connected to ${this.host}`); - - // Need to be overwritten in a subclass.... - return this; - } - - /** - * An internal callback which is triggered when the request timeout timer - * should be started. This is typically whenever a new request is about to - * be send to the server. - * - * @abstract - */ - onStartTimeout() { - throw new Error("Implement onStartTimeout()"); - } - - /** - * An internal callback which is triggered when the request timeout timer - * should be stopped. This is typically whenever a response was received and - * the request was completed. - * - * @abstract - */ - onStopTimeout() { - throw new Error("Implement onStopTimeout()"); - } - - - /** - * Returns the maximal interval in ms between a request and a response. - * The default timeout is 20 seconds - * @returns {int} - * the maximal number of milliseconds - */ - getTimeoutWait() { - - // Apply some self healing magic... - if (!this.timeoutDelay) - return DEFAULT_TIMEOUT; - - return this.timeoutDelay; - } - - /** - * Specifies the maximal interval between a request and a response. If the - * timeout elapsed, all pending request will be canceled and the event queue - * will be cleared. Either the onTimeout() method of the most recent request - * will invoked or in case the request does not support onTimeout() the - * default's listener will be called. - * - * @param {int} interval - * the number of milliseconds before the timeout is triggered. - * Pass null to set the default timeout. - * @returns {SieveAbstractClient} - * a self reference - */ - setTimeoutWait(interval) { - - this.timeoutDelay = interval; - return this; - } - - - /** - * Internal method trigged after a request was completely processed. - * @abstract - */ - onStartIdle() { - throw new Error("Implement onStartIdle()"); - } - - /** - * Internal method triggered when a new request is processed. - * @abstract - */ - onStopIdle() { - throw new Error("Implement onStopIdle()"); - } - - /** - * Gets the maximal number of idle time between two subsequent requests. - * A value of zero indicates idle detection is disabled. - * - * @returns {int} - * the number of ms to wait or null in case idle detection is disabled. - */ - getIdleWait() { - if (!this.idleDelay) - return NO_IDLE; - - return this.idleDelay; - } - - /** - * Specifies the maximal interval between a response and a request. - * If the max time elapsed, the listener's OnIdle() event will be called. - * Thus it can be used for sending "Keep alive" packets. - * - * @param {int} ms - * the maximal number of milliseconds between a response and a request, - * pass null to deactivate. - * @returns {SieveAbstractClient} - * a self reference - */ - setIdleWait(ms) { - if (ms) { - this.idleDelay = ms; - return this; - } - - // No keep alive Packets should be sent, so null the timer and the delay. - this.idleDelay = 0; - this.onStopIdle(); - - return this; - } - - /** - * - * @param {*} listener - */ - addListener(listener) { - this.listener = listener; - } - - /** - * Adds a request to the send queue. - * - * Normal request runs to completion, so they are blocking the queue - * until they are fully processed. If the request fails, the error - * handler is triggered and the request is dequeued. - * - * A greedy request in contrast accepts whatever it can get. Upon an - * error greedy request are not dequeued. They fail silently and the next - * requests is processed. This continues until a request succeeds, a non - * greedy request fails or the queue has no more requests. - * - * @param {SieveAbstractRequest} request - * the request object which should be added to the queue - * - * @param {boolean} [greedy] - * if true requests fail silently - * - * @returns {SieveAbstractClient} - * a self reference - */ - addRequest(request, greedy) { - - // Attach the global bye listener only when needed. - if (!request.byeListener) - if (this.listener && this.listener.onByeResponse) - request.addByeListener(this.listener.onByeResponse); - - // TODO: we should really store this internally, instead of tagging objects - if (greedy) - request.isGreedy = true; - - // Add the request to the message queue - this.requests.push(request); - - // If the message queue was empty, we might have to reinitialize the... - // ... request pump. - - // We can skip this if queue is locked... - if (this.queueLocked) - return this; - - let idx; - // ... or it contains more than one full request - for (idx = 0; idx < this.requests.length; idx++) - if (this.requests[idx].isUnsolicited()) - break; - - if (idx === this.requests.length) - return this; - - if (this.requests[idx] !== request) - return this; - - this._sendRequest(); - - return this; - } - - - /** - * Connects to a ManageSieve server. - * @abstract - * - * @param {string} host - * The target hostname or IP address as String - * @param {int} port - * The target port as Integer - * @param {boolean} secure - * If true, a secure socket will be created. This allows switching to a secure - * connection. - * @param {nsIProxyInfo[]} proxy - * An Array of nsIProxyInfo Objects which specifies the proxy to use. - * Pass an empty array for no proxy. - * Set to null if the default proxy should be resolved. Resolving proxy info is - * done asynchronous. The connect method returns immediately, without any - * information on the connection status... - * Currently only the first array entry is evaluated. - * - * @returns {SieveAbstractClient} - * a self reference - */ - // eslint-disable-next-line no-unused-vars - connect(host, port, secure, proxy) { - throw new Error("Implement me SieveAbstractClient "); - } - - /** - * Cancels all pending request. - * - * @param {Error} [reason] - * the optional reason why the request was canceled. - */ - cancel(reason) { - - while (this.requests.length) - this.requests.shift().cancel(reason); - } - - /** - * Disconnects from the server. - * - * Need to be overwritten. The current implementation is a stub - * which takes care about stopping the timeouts. - * - * @param {Error} [reason] - * the optional reason why the client was disconnected. - */ - disconnect(reason) { - - this.getLogger().logState(`Disconnecting ${this.host}:${this.port}...`); - - this.cancel(reason); - - // free requests... - // this.requests = new Array(); - this.onStopTimeout(); - this.onStopIdle(); - } - - /** - * Called whenever the client enters idle state. - * Which means no request where send for the given idle time. - * - * It emits a signal to external idle listeners. - */ - async onIdle() { - - this.onStopIdle(); - - this.getLogger().logState("libManageSieve/Sieve.js:\nOnIdle"); - - if (this.listener && this.listener.onIdle) - await this.listener.onIdle(); - } - - /** - * Called whenever a request was not responded in a reasonable timeframe. - * It cancel all pending requests and emits a timeout signal to the listeners. - */ - onTimeout() { - - this.onStopTimeout(); - - this.getLogger().logState("libManageSieve/Sieve.js:\nOnTimeout"); - - // clear receive buffer and any pending request... - this.data = null; - - let idx = 0; - while ((idx < this.requests.length) && (this.requests[idx].isGreedy)) - idx++; - - // ... and cancel the active request. It will automatically invoke the ... - // ... request's onTimeout() listener. - if (idx < this.requests.length) { - const request = this.requests[idx]; - this.requests.splice(0, idx + 1); - - request.cancel(new Error("Timeout")); - return; - } - - // in case no request is active, we call the global listener - this.requests = []; - - if (this.listener && this.listener.onTimeout) - this.listener.onTimeout(); - - } - - /** - * Creates a new request parser instance - * @abstract - * - * @param {byte[]} data - * the data to be parsed - * @returns {SieveNodeResponseParser} - * the request parser - */ - createParser(data) { - throw new Error(`Implement SieveAbstractClient::createParser(${data})`); - } - - /** - * Creates a new response builder instance - * @abstract - * - * @returns {SieveAbstractRequestBuilder} - * the response builder. - */ - createRequestBuilder() { - throw new Error("Implement SieveAbstractClient::createRequestBuilder"); - } - - /** - * Called when data was received on the socket. - * - * @param {byte[]} data - * the data received. - */ - onReceive(data) { - - if (this.getLogger().isLevelStream()) - this.getLogger().logStream(`Server -> Client [Byte Array]\n ${data}`); - - if (this.getLogger().isLevelResponse()) - this.getLogger().logResponse(data); - - // responses packets could be fragmented... - if ((this.data === null) || (this.data.length === 0)) - this.data = data; - else - this.data = this.data.concat(data); - - // is a request handler waiting? - if (this.requests.length === 0) - return; - - // first clear the timeout, parsing starts... - this.onStopTimeout(); - - // As we are callback driven, we need to lock the event queue. Otherwise our - // callbacks could manipulate the event queue while we are working on it. - let requests = this._lockMessageQueue(); - - // greedy request take might have an response but do not have to have one. - // They munch what they get. If there's a request they are fine, - // if there's no matching request it's also ok. - let idx = -1; - - while (idx + 1 < requests.length) { - idx++; - const parser = this.createParser(this.data); - - try { - requests[idx].addResponse(parser); - - // We do some cleanup as we don't need the parsed data anymore... - this.data = parser.getByteArray(); - - // parsing was successful, so drop every previous request... - // ... keep in mid previous greedy request continue on exceptions. - requests = requests.slice(idx); - - // we started to parse the request, so it can't be greedy anymore. - } - catch (ex) { - // request could be fragmented or something else, as it's greedy, - // we don't care about any exception. We just log them in oder - // to make debugging easier.... - if (this.getLogger().isLevelState()) { - this.getLogger().logState(`Parsing Warning in libManageSieve/Sieve.js:\\n${ex.toString()}`); - this.getLogger().logState(ex.stack); - } - - // a greedy request might or might not get an request, thus - // it's ok if it fails - if (requests[idx].isGreedy) - continue; - - // ... but a non greedy response must parse without an error. Otherwise... - // ... something is broken. this is most likely caused by a fragmented ... - // ... packet, but could be also a broken response. So skip processing... - // ... and restart the timeout. Either way the next packet or the ... - // ... timeout will resolve this situation for us. - - this._unlockMessageQueue(requests); - this.onStartTimeout(); - - return; - } - - // Request is completed... - if (!requests[0].hasNextRequest()) { - // so remove it from the event queue. - const request = requests.shift(); - // and update the index - idx--; - - // ... if it was greedy, we munched an unexpected packet... - // ... so there is still a valid request dangling around. - if (request.isGreedy) - continue; - } - - if (!parser.isEmpty()) - continue; - - this._unlockMessageQueue(requests); - - - // Are there any other requests waiting in the queue. - - // TODO FIX ME should always be dispatched, to relax the main thread. - // But in mozilla modules we don't have access to a window object and - // timeouts are more complicated. - - // var that = this; - // window.setTimeout(function () {that._sendRequest()}, 0); - - this._sendRequest(); - - return; - } - - - // we end up here only if all responses where greedy or there were no... - // ... response parser at all. Thus all we can do is release the message... - // ... queue, cache the data and wait for a new response to be added. - - this._unlockMessageQueue(requests); - - this.getLogger().logState("Skipping Event Queue"); - } - - /** - * - */ - _sendRequest() { - - let idx = 0; - while (idx < this.requests.length) { - if (this.requests[idx].isUnsolicited()) - break; - - idx++; - } - - if (idx >= this.requests.length) - return; - - // start the timeout, before sending anything. So that we will timeout... - // ... in case the socket is jammed... - this.onStartTimeout(); - - const output = this.requests[idx].getNextRequest(this.createRequestBuilder()).getBytes(); - - if (this.getLogger().isLevelRequest()) - this.getLogger().logRequest(`Client -> Server:\n${output}`); - - this.onSend(output); - - return; - } - - /** - * Called everytime data is ready to send. - * @abstract - * - * @param {object} data - * the data to send to the server. - */ - onSend(data) { - throw new Error(`Implement SieveAbstractClient::onSend(${data})`); - } - - /** - * - */ - _lockMessageQueue() { - this.queueLocked = true; - const requests = this.requests.concat(); - - this.requests = []; - - return requests; - } - - /** - * - * @param {*} requests - */ - _unlockMessageQueue(requests) { - this.requests = requests.concat(this.requests); - this.queueLocked = false; - } - } - - exports.SieveAbstractClient = SieveAbstractClient; - -})(module.exports || this); diff --git a/src/common/libManageSieve/SieveAbstractClient.mjs b/src/common/libManageSieve/SieveAbstractClient.mjs new file mode 100644 index 00000000..adee86d5 --- /dev/null +++ b/src/common/libManageSieve/SieveAbstractClient.mjs @@ -0,0 +1,1034 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +/* + * This class is a simple socket implementation for the manage sieve protocol. + * Due to the asymmetric nature of the Mozilla sockets we need message queue. + * <p> + * New requests are added via the "addRequest" method. In case of a response, + * the corresponding request will be automatically called back via its + * "addResponse" method. + * <p> + * If you need a secure connection, set the flag secure in the constructor. + * Then connect to the host. And invoke the "startTLS" Method as soon as you + * negotiated the switch to a encrypted connection. After calling startTLS + * Mozilla will immediately switch to an encrypted connection. + * <p> + */ + +import { SieveTimer } from "./SieveTimer.mjs"; +import { SieveResponseParser } from "./SieveResponseParser.mjs"; +import { SieveRequestBuilder } from "./SieveRequestBuilder.mjs"; + +const DEFAULT_TIMEOUT = 20000; +const NO_IDLE = 0; + + +const NOT_STARTED = -1; + +/** + * Implements a locked message queue. + */ +class LockedMessageQueue { + + /** + * Creates a new instance and moves all of the given requests + * into the locked queue. + * + * @param {SieveAbstractRequest[]} items + * the data which should be moved. + */ + constructor(items) { + this.offset = NOT_STARTED; + this.items = [...items.splice(0, items.length)]; + } + + /** + * Removes all remaining element from the queue and returns them + * as array + * + * @returns {SieveAbstractRequest[]} + * all remaining items. + */ + dequeue() { + let offset = this.offset; + + if (offset === NOT_STARTED) + offset = 0; + + this.offset = NOT_STARTED; + return this.items.splice(offset, this.items.length); + } + + /** + * Used to drain the message queue. Marks all all pending requests as abandoned + * This typically happens when the connection was lost or upon an + * forced disconnect. + * + * @param {object} reason + * the reason why the message queue was abandoned. Typically an exception. + */ + shutdown(reason) { + for (const item of this.items) + item.abandon(reason); + } + + /** + * Truncates the queue on the current position. + * The current element plus all previous elements will be removed. + * The queue iterator will be reset to not started; + */ + trunc() { + if (this.offset === NOT_STARTED) + return; + + this.items.splice(0, this.offset + 1); + this.reset(); + } + + /** + * Returns the remaining number of elements contained inside this queue. + * + * @returns {int} + * the number of remaining elements. + */ + length() { + if (this.offset === NOT_STARTED) + return this.items.length; + + return this.items.length - this.offset; + } + + /** + * Resets the iterator to not started. + */ + reset() { + this.offset = NOT_STARTED; + } + + /** + * Checks if the queue has remaining elements. + * + * @returns {boolean} + * true in case there are more elements. False in case end was reached. + */ + hasNext() { + return (this.offset + 1 < this.items.length); + } + + /** + * Returns the queues next element. + * + * @returns {SieveAbstractRequest} + * the next queued request. + */ + getNext() { + this.offset++; + return this.items[this.offset]; + } +} + +/** + * Implements a simple message queue logic. + */ +class MessageQueue { + + /** + * Creates a new message queue instance. + */ + constructor() { + this.queued = []; + this.locked = null; + this.canceled = false; + } + + /** + * Adds a new request to the end of the message queue + * + * @param {SieveAbstractRequest} request + * the request to be added. + * + * @returns {MessageQueue} + * a self reference. + */ + enqueue(request) { + this.queued.push(request); + return this; + } + + /** + * Used to drain the message queue. Marks all all enqueued requests as abandoned. + * + * @param {string} reason + * the human readable string why the message queue was abandoned. + * + * @returns {MessageQueue} + * a self reference. + */ + shutdown(reason) { + + if (this.isLocked()) + this.getLock().shutdown(reason); + + while (this.queued.length) + this.queued.shift().abandon(reason); + + return this; + } + + /** + * Checks if the message queue has non completed request. + * + * @returns {boolean} + * true in case the queue is locked otherwise false. + */ + isEmpty() { + if (this.isLocked() && this.locked.length()) + return false; + + return (this.queued.length === 0); + } + + /** + * Checks if the message queue is currently busy. + * + * @returns {boolean} + * true in case the message queue is busy otherwise false. + */ + isLocked() { + return (this.locked !== null); + } + + /** + * Returns the locked and protected message queue. + * It will work only in case the queue is locked. + * Otherwise and exception will be thrown. + * + * @returns {LockedMessageQueue} + * the currently locked message queue. + */ + getLock() { + if (!this.locked) + throw new Error("Message queue is not locked"); + + return this.locked; + } + + /** + * Locks and protects the current message queue from changes. + * + * @returns {MessageQueue} + * a self reference + */ + lock() { + + if (this.isLocked()) + throw new Error("Message queue is already locked"); + + this.locked = (new LockedMessageQueue(this.queued)); + return this; + } + + /** + * Unlocks the message queue, and allows changes. + * + * @returns {MessageQueue} + * a self reference + */ + unlock() { + if (!this.isLocked()) + return this; + + // Copy all entries from the locked queue back into our normal queue. + this.queued = [...this.locked.dequeue(), ...this.queued]; + this.locked = null; + + return this; + } + + /** + * Checks if the queue is unlocked and if there are any + * queued request which are ready to be processed. + * + * @returns {SieveAbstractRequest} + * the next element or null in case the queue is locked or no + * request is enqueued. + */ + peek() { + // The queue is locked this means we should not send anything. + if (this.isLocked()) + return null; + + // Check if there is any request which can be send out. + for (const item of this.queued) { + if (!item.hasRequest()) + continue; + + return item; + } + + return null; + } +} + +/** + * A simple double buffer implementation. + * + * JavaScript is single threaded but async. Which means when deferring + * a call it can happen that some one else modifies the buffer. + * + * In order to prevent this we use two buffers one buffer which can be + * always safely written and an other buffer which can be always read. + * + * When calling flush the write buffer will be transferred into the read buffer + * Calling trunc cleans the read buffer. + */ +class DoubleBuffer { + + /** + * Creates a new instance. + */ + constructor() { + this.writer = []; + this.reader = []; + } + + /** + * Stores data inside the double buffer. + * + * @param {byte[]} data + * the data to be stored inside the writer + * @returns {DoubleBuffer} + * a self reference. + */ + write(data) { + this.writer.push(...data); + return this; + } + + /** + * Returns a reference to the data stored inside the reader. + * It will update and sync the reader buffer with the writer buffer + * before returning. + * + * It does not change or consume any reader buffer data. + * To shrink or truncate the reader buffer call truncate. + * + * @returns {byte[]} + * the data stored inside the reader. + */ + read() { + // We need the reader length to determine the number of new bytes. + const offset = this.reader.length; + + // Copy the writer into the reader. + this.reader.push(...this.writer); + // an then shrink the writer. + this.writer.splice(0, this.reader.length - offset); + + return this.reader; + } + + /** + * Truncates the read buffer + * + * @param {int} count + * the number of bytes to be truncated + * @returns {DoubleBuffer} + * a self reference + */ + trunc(count) { + this.reader.splice(0, count); + return this; + } + + /** + * Checks if the write buffer is dirty. + * This means new data has arrived and wait to be processed. + * + * @returns {boolean} + * true in case the write buffer is dirty otherwise false; + */ + isDirty() { + return (this.writer.length !== 0); + } + + /** + * Clears the read as well as the write buffer. + * + * @returns {DoubleBuffer} + * a self reference + */ + clear() { + this.reader = []; + this.writer = []; + + return this; + } + + /** + * Returns the total buffer length, the sum of the read plus the write buffer. + * + * @returns {int} + * the total buffer length + */ + length() { + return this.reader.length + this.writer.length; + } +} + +/** + * An abstract implementation for the manage sieve protocol. + * + * It implements a message pump and parsing facility. + * Only the connections to the transport are needed to be implemented. + * + * The javascript syntax for this code is extremely limited. + * As this code is used for a mozilla module as well as in node js. + * + * Due to various limitation there is no window object and also no toSource(). + * Same applies to timeouts. They need to be implemented with platform + * specific code. + * + * In general you should avoid the "new" operator as this makes imports difficult. + * Mozilla's Modules, Node's Require and the new ES imports are mostly + * incompatible to each other. + * + */ +class SieveAbstractClient { + + /** + * Creates a new instance + */ + constructor() { + this.host = null; + this.port = null; + + this.socket = null; + this.buffer = new DoubleBuffer(); + this.queue = new MessageQueue(); + + this.requests = []; + + this.idleDelay = 0; + + this.timeoutTimer = new SieveTimer(); + this.idleTimer = new SieveTimer(); + + + // out of the box we support the following manage sieve commands... + // ... the server might advertise additional commands they are added ... + // ... or removed by the set compatibility method + this.compatibility = { + authenticate: true, + starttls: true, + logout: true, + capability: true, + // until now we do not support havespace... + // havespace : false, + putscript: true, + listscripts: true, + setactive: true, + getscript: true, + deletescript: true + }; + } + + /** + * Gives this socket a hint, whether a sieve commands is supported or not. + * + * Setting the corresponding attribute to false, indicates, that a sieve command + * should not be used. As this is only an advice, such command will still be + * processed by this sieve socket. + * + * By default the socket seek maximal compatibility. + * + * @param {object} capabilities commands + * the supported sieve commands as an associative array. Attribute names have + * to be in lower case, the values can be either null, undefined, true or false. + * + * @example + * sieve.setCompatibility({checkscript:true, rename:true, starttls:false}); + */ + setCompatibility(capabilities) { + for (const capability in capabilities) + this.compatibility[capability] = capabilities[capability]; + } + + /** + * Returns a list of supported sieve commands. As the socket seeks + * maximal compatibility, it always suggest the absolute minimal sieve + * command set defined in the rfc. This value is only a hint, and does + * not represent the server's capabilities! + * + * A command is most likely unsupported if the corresponding attribute is null and + * disabled if the the attribute is false + * + * You should override these defaults as soon as possible. + * + * @returns {Struct} + * an associative array structure indicating supported sieve command. + * Unsupported commands are indicated by a null, disabled by false value... + * + * @example + * if (sieve.getCompatibility().putscript) { + * // put script command supported... + * } + */ + getCompatibility() { + return this.compatibility; + } + + + /** + * Gets a reference to the current logger + * @returns {SieveLogger} + * the current logger + * + * @abstract + */ + getLogger() { + throw new Error("Implement getLogger()"); + } + + /** + * Checks if the connection to the server is still alive and can be used to send + * and receive messages + * @returns {boolean} + * true in case the connection is alive otherwise false + */ + isAlive() { + if (!this.socket) + return false; + + return true; + } + + /** + * Check is the connection supports any connection security. + * It could be either disabled by the client or the server. + * + * @abstract + * + * @returns {boolean} + * true in case the connection can be or is secure otherwise false + */ + isSecure() { + throw new Error("Implement isSecure()"); + } + + /** + * This method secures the connection to the sieve server. By activating + * Transport Layer Security all Data exchanged is encrypted. + * + * Before calling this method you need to request a encrypted connection by + * sending a startTLSRequest. Invoke this method immediately after the server + * confirms switching to TLS. + * + * @returns {SieveAbstractClient} + * a self reference + **/ + startTLS() { + if (!this.isSecure()) + throw new Error("TLS can't be started no secure socket"); + + if (!this.socket) + throw new Error(`Can't start TLS, your are not connected to ${this.host}`); + + // Need to be overwritten in a subclass.... + return this; + } + + /** + * An internal callback which is triggered when the request timeout timer + * should be started. This is typically whenever a new request is about to + * be send to the server. + * + * @abstract + */ + onStartTimeout() { + + this.getLogger().logState("[SieveAbstractClient:onStartTimeout()] Starting/Restarting timeout"); + // clear any existing timeouts + this.getTimeoutTimer().cancel(); + + // ensure the idle timer is stopped + this.onStopIdle(); + + // then restart the timeout timer. + this.getTimeoutTimer().start( + () => { this.onTimeout(); }, + this.getTimeoutWait()); + } + + /** + * An internal callback which is triggered when the request timeout timer + * should be stopped. This is typically whenever a response was received and + * the request was completed. + * + * @abstract + */ + onStopTimeout() { + this.getLogger().logState("[SieveAbstractClient:onStopTimeout()] Stopping timeout"); + + // clear any existing timeouts. + this.getTimeoutTimer().cancel(); + + // and start the idle timer + this.onStartIdle(); + } + + + /** + * Returns the maximal interval in ms between a request and a response. + * The default timeout is 20 seconds + * @returns {int} + * the maximal number of milliseconds + */ + getTimeoutWait() { + + // Apply some self healing magic... + if (!this.timeoutDelay) + return DEFAULT_TIMEOUT; + + return this.timeoutDelay; + } + + /** + * Specifies the maximal interval between a request and a response. If the + * timeout elapsed, all pending request will be canceled and the event queue + * will be cleared. Either the onTimeout() method of the most recent request + * will invoked or in case the request does not support onTimeout() the + * default's listener will be called. + * + * @param {int} interval + * the number of milliseconds before the timeout is triggered. + * Pass null to set the default timeout. + * @returns {SieveAbstractClient} + * a self reference + */ + setTimeoutWait(interval) { + + this.timeoutDelay = interval; + return this; + } + + /** + * Returns the timer used to track timeouts. + * It is guaranteed to be non null. + * + * @returns {SieveTimer} + * the current timeout timer + */ + getTimeoutTimer() { + return this.timeoutTimer; + } + + /** + * Returns the timer used to track idle. + * It is guaranteed to be non null. + * + * @returns {SieveTimer} + * the current idle timer. + */ + getIdleTimer() { + return this.idleTimer; + } + + /** + * Internal method trigged after a request was completely processed. + * @abstract + */ + onStartIdle() { + // first ensure the timer is stopped.. + this.onStopIdle(); + + // ... then configure the timer. + const delay = this.getIdleWait(); + + if (!delay) + return; + + this.getIdleTimer().start(() => { this.onIdle(); }, delay); + } + + /** + * Internal method triggered when a new request is processed. + */ + onStopIdle() { + this.getIdleTimer().cancel(); + } + + /** + * Gets the maximal number of idle time between two subsequent requests. + * A value of zero indicates idle detection is disabled. + * + * @returns {int} + * the number of ms to wait or null in case idle detection is disabled. + */ + getIdleWait() { + if (!this.idleDelay) + return NO_IDLE; + + return this.idleDelay; + } + + /** + * Specifies the maximal interval between a response and a request. + * If the max time elapsed, the listener's OnIdle() event will be called. + * Thus it can be used for sending "Keep alive" packets. + * + * @param {int} ms + * the maximal number of milliseconds between a response and a request, + * pass null to deactivate. + * @returns {SieveAbstractClient} + * a self reference + */ + setIdleWait(ms) { + if (ms) { + this.idleDelay = ms; + return this; + } + + // No keep alive Packets should be sent, so null the timer and the delay. + this.idleDelay = 0; + this.onStopIdle(); + + return this; + } + + /** + * Sets the callback listener. + * @param {*} listener + */ + addListener(listener) { + this.listener = listener; + } + + /** + * Adds a request to the send queue. + * + * Normal request runs to completion, so they are blocking the queue + * until they are fully processed. If the request fails, the error + * handler is triggered and the request is dequeued. + * + * @param {SieveAbstractRequest} request + * the request object which should be added to the queue + * + * @returns {SieveAbstractClient} + * a self reference + */ + async addRequest(request) { + + // Attach the global bye listener only when needed. + if (!request.byeListener) + if (this.listener && this.listener.onByeResponse) + request.addByeListener(this.listener.onByeResponse); + + // Add the request to the message queue + this.queue.enqueue(request); + await this._sendRequest(); + + return this; + } + + + /** + * Connects to a ManageSieve server. + * @abstract + * + * @param {string} host + * The target hostname or IP address as String + * @param {int} port + * The target port as Integer + * @param {boolean} secure + * If true, a secure socket will be created. This allows switching to a secure + * connection. + * + * @returns {SieveAbstractClient} + * a self reference + */ + // eslint-disable-next-line no-unused-vars + connect(host, port, secure) { + throw new Error("Implement me SieveAbstractClient "); + } + + /** + * Cancels all pending request. + * + * @param {Error} [reason] + * the optional reason why the request was canceled. + */ + cancel(reason) { + + this.getLogger().logState(`[SieveAbstractClient:cancel()] Shutting down message queue ${reason}`); + + // Mark the queue as canceled... + this.queue.shutdown(reason); + + // ... and then retrigger the queue with an empty message. + // does not hurt... + this.onReceive([]); + } + + /** + * Disconnects from the server. + * + * Need to be overwritten. The current implementation is a stub + * which takes care about stopping the timeouts. + * + * @param {Error} [reason] + * the optional reason why the client was disconnected. + */ + async disconnect(reason) { + + this.getLogger().logState(`SieveAbstractClient: Disconnecting ${this.host}:${this.port}...`); + + this.getIdleTimer().cancel(); + this.getTimeoutTimer().cancel(); + + this.cancel(reason); + } + + /** + * Called whenever the client enters idle state. + * Which means no request where send for the given idle time. + * + * It emits a signal to external idle listeners. + */ + async onIdle() { + + this.onStopIdle(); + + this.getLogger().logState("libManageSieve/Sieve.js:\nOnIdle"); + + if (this.listener && this.listener.onIdle) + await this.listener.onIdle(); + } + + /** + * Called whenever a request was not responded in a reasonable time frame. + * It cancel all pending requests and emits a timeout signal to the listeners. + */ + onTimeout() { + this.getLogger().logState("[SieveAbstractClient:onTimeout()] Timeout fired"); + + this.onStopTimeout(); + + this.queue.shutdown(new Error("Timeout")); + + return; + } + + /** + * Creates a new request parser instance + * + * @param {byte[]} data + * the data to be parsed + * @returns {SieveResponseParser} + * the request parser + */ + createParser(data) { + return new SieveResponseParser(data); + } + + /** + * Creates a new response builder instance + * + * @returns {SieveRequestBuilder} + * the response builder. + */ + createRequestBuilder() { + return new SieveRequestBuilder(); + } + + + /** + * Called when data was received on the socket. + * + * @param {byte[]} data + * the data received. + */ + async onReceive(data) { + + this.getLogger().logState("[SieveAbstractClient:onReceive] Starting processing received data..."); + + if (this.getLogger().isLevelStream()) + this.getLogger().logStream(`[SieveAbstractClient:onReceive] Server -> Client [Byte Array]\n ${data}`); + + if (this.getLogger().isLevelResponse()) + this.getLogger().logResponse(data); + + // responses packets could be fragmented... + this.getLogger().logState("[SieveAbstractClient:onReceive] ... add data to buffer..."); + this.buffer.write(data); + + // is a request handler waiting? + if (this.queue.isEmpty()) { + this.getLogger().logState("[SieveAbstractClient:onReceive] ... skipping, no request handler ready."); + return; + } + + if (this.queue.isLocked()) { + this.getLogger().logState("[SieveAbstractClient:onReceive] ... skipping queue is locked."); + return; + } + + // first clear the timeout, parsing starts... + this.onStopTimeout(); + + // Sound strange but as we are async, we need to lock the event queue. + // Otherwise an async function may manipulate the event queue while + // we are waiting for a callback. + this.getLogger().logState("[SieveAbstractClient:onReceive] ... locking Message Queue ..."); + + try { + const lock = this.queue.lock().getLock(); + + while (lock.hasNext()) { + + const request = lock.getNext(); + + if (request.isAbandoned()) { + this.getLogger().logState("[SieveAbstractClient:onReceive] ... skipping abandoned request ..."); + await request.onAbandoned(); + this.buffer.clear(); + lock.trunc(); + continue; + } + + // We can bail out in case we ran out of data + if (!this.buffer.length()) { + this.getLogger().logState("[SieveAbstractClient:onReceive] ... ran out of data, waiting for more"); + this.onStartTimeout(); + return; + } + + this.getLogger().logState(`[SieveAbstractClient:onReceive] ` + + `Start parsing ${this.buffer.length()}`); + + try { + const parser = this.createParser(this.buffer.read()); + + await (request.onResponse(parser)); + + // We do some cleanup as we don't need the parsed data anymore... + this.buffer.trunc(parser.getPosition()); + + this.getLogger().logState(`[SieveAbstractClient:onReceive] ` + + `Parsing successful, remaining ${this.buffer.length()} bytes`); + + } catch (ex) { + + if (request.isOptional()) { + this.getLogger().logState(`[SieveAbstractClient:onReceive] ` + + `... failed but is optional, skipping to next request`); + + continue; + } + + // Parsing the response failed. This is most likely caused by fragmentation + // and will be resolved as soon as the remaining bytes arrive. + // + // In case it is really a syntax error the we will run into a timeout. + // + // So in either way the next packet or the timeout will resolve this + // situation for us. + + if (this.getLogger().isLevelState()) { + this.getLogger().logState(`Parsing Warning in libManageSieve/Sieve.js:\\n${ex.toString()}`); + this.getLogger().logState(ex.stack); + } + + // In case the buffer is dirty new data arrived while we were parsing + // and we are ready to try it again. + if (this.buffer.isDirty()) { + this.getLogger().logState(`[SieveAbstractClient:onReceive] ` + + `... buffers are dirty, restarting parsing.`); + + lock.reset(); + continue; + } + + // Restore the message queue and restart the timer. + this.onStartTimeout(); + this.getLogger().logState("[SieveAbstractClient:onReceive] Waiting for more data to continue"); + return; + } + + // The request was processed but it is not yet completed because + // is needs to send a new response. + // + // This means we need to stop here so that the message queue restarts. + if (request.hasNextRequest()) { + // First drop all processed request and then restart the processing + lock.reset(); + break; + } + + // The request was processed and is completed. So let's get rid of it. + lock.trunc(); + + this.getLogger().logState(`[SieveAbstractClient:onReceive] ` + + `Removing request from queue ${lock.length()}, ${this.buffer.length()}`); + } + } finally { + this.getLogger().logState("[SieveAbstractClient:onReceive] ... unlocking Message Queue ..."); + this.queue.unlock(); + } + + // Finally we need to check if a new request arrived while we were + // parsing the response and restart the message processing + if (!this.queue.isEmpty()) { + this.getLogger().logState("[SieveAbstractClient:onReceive] Restarting request processing"); + await this._sendRequest(); + } + + this.getLogger().logState("[SieveAbstractClient:onReceive] Finished processing received data"); + } + + /** + * Send the next request, if available. + */ + async _sendRequest() { + + const request = this.queue.peek(); + + if (!request) + return; + + // start the timeout, before sending anything. So that we will timeout... + // ... in case the socket is jammed... + this.onStartTimeout(); + + const output = (await request.getNextRequest(this.createRequestBuilder())).getBytes(); + + if (this.getLogger().isLevelRequest()) + this.getLogger().logRequest(`Client -> Server:\n${output}`); + + this.onSend(output); + + return; + } + + /** + * Called everytime data is ready to send. + * @abstract + * + * @param {object} data + * the data to send to the server. + */ + onSend(data) { + throw new Error(`Implement SieveAbstractClient::onSend(${data})`); + } +} + +export { SieveAbstractClient }; diff --git a/src/common/libManageSieve/SieveAbstractCrypto.js b/src/common/libManageSieve/SieveAbstractCrypto.js deleted file mode 100755 index 76758b88..00000000 --- a/src/common/libManageSieve/SieveAbstractCrypto.js +++ /dev/null @@ -1,192 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - const MIN_SALT_LENGTH = 2; - const MIN_ITERATION_COUNT = 0; - - const MAX_CHAR_CODE = 255; - - const HEX_STRING = 16; - - /** - * Crypto implementations are very browser specific. - * Which means we need a separate wrapper for each browser. - */ - class SieveAbstractCrypto { - - /** - * Creates a new crypto wrapper. - * @param {string} name - * the crypto algorithms name. - */ - constructor(name) { - this.name = name; - } - - /** - * Converts a byte array into a hex string - * @param {byte[]} tmp - * the byte array which should be converted. - * @returns {string} - * the hex string - * @abstract - */ - byteArrayToHexString(tmp) { - let str = ""; - for (let i = 0; i < tmp.length; i++) - str += ("0" + tmp[i].toString(HEX_STRING)).slice(-2); - - return str; - } - - /** - * Converts a byte array into a string - * - * @param {byte[]} bytes - * the byte array which should be converted. - * @returns {string} - * the converted string - */ - byteArrayToStr(bytes) { - let result = ""; - - if (Array.isArray(bytes) === false) - throw new Error("Parameter bytes is not a byte array"); - - for (let i = 0; i < bytes.length; i++) { - const byte = String.fromCharCode(bytes[i]); - if (byte > MAX_CHAR_CODE) - throw new Error(`Byte Array Invalid: ${byte}`); - - result += byte; - } - - return result; - } - - /** - * Converts a binary string into a byte array. - * - * @param {string} str - * the string which should be converted. - * @returns {byte[]} - * the converted string in byte array representation - */ - strToByteArray(str) { - const result = []; - - for (let i = 0; i < str.length; i++) { - if (str.charCodeAt(i) > MAX_CHAR_CODE) - throw new Error(`Invalid Characters for Binary String: ${str.charCodeAt(i)}`); - - result.push(str.charCodeAt(i)); - } - - return result; - } - - /** - * Calculates the HMAC keyed hash for the given algorithm. - * @abstract - * - * @param {byte[]} key - * The key as octet string - * @param {byte[]|string} bytes - * The input string as byte array or string - * @returns {byte[]} - * the calculated HMAC keyed hash for the given input string. E.g. HMAC-SHA-1 hashes are - * always always 20 octets long. - */ - HMAC(key, bytes) { - throw new Error(`Implement HMAC Algorithm for ${this.name} with key ${key} and data ${bytes}`); - } - - - /** - * Calculates the Hash for the given algorithm. - * @abstract - * - * @param {bytes[]|string} bytes - * The input string as byte array or string - * @returns {byte[]} - * the calculated hash value for the input string.s - */ - H(bytes) { - throw new Error(`Implement Hashing Algorithm for ${this.name} with data ${bytes}`); - } - - /** - * Hi(str, salt, i) is a PBKDF2 [RFC2898] implementation with HMAC() as the - * pseudorandom function (PRF) and with dkLen == output length of HMAC() == output - * length of H(). - * - * "str" is an octet input string while salt is a random octet string. - * "i" is the iteration count, "+" is the string concatenation operator, - * and INT(1) is a 4-octet encoding of the integer with the value 1. - * - * Hi(str, salt, i): - * - * U1 := HMAC(str, salt + INT(1)) - * U2 := HMAC(str, U1) - * ... - * Ui-1 := HMAC(str, Ui-2) - * Ui := HMAC(str, Ui-1) - * - * Hi := U1 XOR U2 XOR ... XOR Ui - * - * @param {byte[]|string} str - * an octet input string - * @param {byte[]|string} salt - * random octet string - * @param {int} i - * iteration count a positive number (>= 1), suggested to be at least 4096 - * - * @returns {byte[]} - * the pseudorandom value as byte string - */ - Hi(str, salt, i) { - - if (Array.isArray(str) === false) - str = this.strToByteArray(str); - - if (Array.isArray(salt) === false) - salt = this.strToByteArray(salt); - - if (salt.length < MIN_SALT_LENGTH) - throw new Error("Insufficient salt"); - - if (i <= MIN_ITERATION_COUNT) - throw new Error("Invalid Iteration counter"); - - salt.push(0, 0, 0, 1); - - salt = this.HMAC(str, salt); - - const hi = salt; - - while (--i) { - salt = this.HMAC(str, salt); - - for (let j = 0; j < hi.length; j++) - hi[j] ^= salt[j]; - } - - return hi; - } - } - - exports.SieveAbstractCrypto = SieveAbstractCrypto; - -})(module.exports || this); diff --git a/src/common/libManageSieve/SieveAbstractCrypto.mjs b/src/common/libManageSieve/SieveAbstractCrypto.mjs new file mode 100644 index 00000000..08c77feb --- /dev/null +++ b/src/common/libManageSieve/SieveAbstractCrypto.mjs @@ -0,0 +1,245 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +const MIN_SALT_LENGTH = 2; +const MIN_ITERATION_COUNT = 0; + +const MAX_CHAR_CODE = 255; + +const HEX_STRING = 16; + +const HASH_SHA1 = "SHA-1"; +const HASH_SHA256 = "SHA-256"; +const HASH_SHA512 = "SHA-512"; + +// eslint-disable-next-line no-magic-numbers +const MAGIC_SALT = [0, 0, 0, 1]; + + +/** + * Crypto implementations are very browser specific. + * Which means we need a separate wrapper for each browser. + */ +class SieveAbstractCrypto { + + /** + * Creates a new crypto wrapper. + * @param {string} name + * the crypto algorithms name. + */ + constructor(name) { + + if ((name !== HASH_SHA1) && (name !== HASH_SHA256) && (this.name !== HASH_SHA512)) + throw new Error(`Unknown Hash algorithm ${name}`); + + this.name = name; + } + + /** + * Returns the hashing algorithm. + * In case the algorithm is unknown an exception is thrown. + * + * @returns {string} + * the hash algorithm + */ + getCryptoHash() { + return this.name; + } + + /** + * Converts a byte array into a hex string + * @param {byte[]} tmp + * the byte array which should be converted. + * @returns {string} + * the hex string + * @abstract + */ + byteArrayToHexString(tmp) { + let str = ""; + for (let i = 0; i < tmp.length; i++) + str += ("0" + tmp[i].toString(HEX_STRING)).slice(-2); + + return str; + } + + /** + * Converts a byte array into a string + * + * @param {byte[]} bytes + * the byte array which should be converted. + * @returns {string} + * the converted string + */ + byteArrayToStr(bytes) { + let result = ""; + + if (Array.isArray(bytes) === false) + throw new Error("Parameter bytes is not a byte array"); + + for (let i = 0; i < bytes.length; i++) { + const byte = String.fromCharCode(bytes[i]); + if (byte > MAX_CHAR_CODE) + throw new Error(`Byte Array Invalid: ${byte}`); + + result += byte; + } + + return result; + } + + /** + * Converts a binary string into a byte array. + * + * @param {string} str + * the string which should be converted. + * @returns {byte[]} + * the converted string in byte array representation + */ + strToByteArray(str) { + const result = []; + + for (let i = 0; i < str.length; i++) { + if (str.charCodeAt(i) > MAX_CHAR_CODE) + throw new Error(`Invalid Characters for Binary String: ${str.charCodeAt(i)}`); + + result.push(str.charCodeAt(i)); + } + + return result; + } + + /** + * Calculates the HMAC keyed hash for the given algorithm. + * @abstract + * + * @param {byte[]} key + * The key as octet string + * @param {byte[]|string} bytes + * The input string as byte array or string + * @returns {byte[]} + * the calculated HMAC keyed hash for the given input string. E.g. HMAC-SHA-1 hashes are + * always always 20 octets long. + */ + async HMAC(key, bytes) { + throw new Error(`Implement HMAC Algorithm for ${this.name} with key ${key} and data ${bytes}`); + } + + + /** + * Calculates the Hash for the given algorithm. + * @abstract + * + * @param {bytes[]|string} bytes + * The input string as byte array or string + * @returns {byte[]} + * the calculated hash value for the input string.s + */ + async H(bytes) { + throw new Error(`Implement Hashing Algorithm for ${this.name} with data ${bytes}`); + } + + /** + * Hi(str, salt, i) is a PBKDF2 [RFC2898] implementation with HMAC() as the + * pseudorandom function (PRF) and with dkLen == output length of HMAC() == output + * length of H(). + * + * "str" is an octet input string while salt is a random octet string. + * "i" is the iteration count, "+" is the string concatenation operator, + * and INT(1) is a 4-octet encoding of the integer with the value 1. + * + * Hi(str, salt, i): + * + * U1 := HMAC(str, salt + INT(1)) + * U2 := HMAC(str, U1) + * ... + * Ui-1 := HMAC(str, Ui-2) + * Ui := HMAC(str, Ui-1) + * + * Hi := U1 XOR U2 XOR ... XOR Ui + * + * @param {Uint8Array} key + * an octet input string + * @param {Uint8Array} salt + * random octet string + * @param {int} iterations + * iteration count a positive number (>= 1), suggested to be at least 4096 + * + * @returns {Uint8Array} + * the pseudorandom value as byte string + */ + async Hi(key, salt, iterations) { + + if (!(key instanceof Uint8Array)) + throw new Error("Key not an Uint8Array"); + + if (!(salt instanceof Uint8Array)) + throw new Error("Salt not an Uint8Array"); + + if (salt.length < MIN_SALT_LENGTH) + throw new Error("Insufficient salt"); + + if (iterations <= MIN_ITERATION_COUNT) + throw new Error("Invalid Iteration counter"); + + salt = new Uint8Array([...salt, ...MAGIC_SALT]); + + salt = await this.HMAC(key, salt); + + const hi = salt; + + while (--iterations) { + salt = await this.HMAC(key, salt); + + for (let j = 0; j < hi.length; j++) + hi[j] ^= salt[j]; + } + + return new Uint8Array(hi); + } + + /** + * Applies the SASLprep profile [RFC4013] of the"stringprep" algorithm + * [RFC3454] to the given UTF-8 encoded byte array. + * + * The resulting byte array is also in UTF-8. + * + * When applying SASLprep, "str" is treated as a "stored strings", which + * means that unassigned Unicode codepoints are prohibited. + * + * @param {Uint8Array} data + * the string to be normalized + * @returns {Uint8Array} + * the normalized string + */ + normalize(data) { + return data; + /* + // RFC 4013 and 3454 + C12 -> "" + B1 -> "" + + str = str.normalize("NFKC"); + + // Illegal code points + C12, C21, C22, C3, C4, C5, C6, C7, C8, C9, A1 + -> throw + + // D1 && D2 + -> throw + + // D1 is allowed only one as first or last character + */ + } +} + +export { + SieveAbstractCrypto +}; diff --git a/src/common/libManageSieve/SieveAbstractLogger.js b/src/common/libManageSieve/SieveAbstractLogger.js deleted file mode 100755 index 26c77f6e..00000000 --- a/src/common/libManageSieve/SieveAbstractLogger.js +++ /dev/null @@ -1,283 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - const TWO_CHARS = 2; - const THREE_CHARS = 3; - - const BASE_10 = 10; - - // eslint-disable-next-line no-magic-numbers - const LOG_REQUEST = (1 << 0); - // eslint-disable-next-line no-magic-numbers - const LOG_RESPONSE = (1 << 1); - // eslint-disable-next-line no-magic-numbers - const LOG_STATE = (1 << 2); - // eslint-disable-next-line no-magic-numbers - const LOG_STREAM = (1 << 3); - // eslint-disable-next-line no-magic-numbers - const LOG_SESSION_INFO = (1 << 4); - - const DEFAULT_LEVEL = 0; - - /** - * Implements a common and platform independent logging interface. - * The log level is interpreted as a bit filed with turns logging - * for the specified scope on and of. - * - * The level is concerning scopes and does not differentiate between - * warning, error and info. - */ - class SieveAbstractLogger { - - /** - * Creates a new instance - * @param {string} [prefix] - * an optional prefix for this logger. - * @param {int} [level] - * the logger level - * - */ - constructor(prefix, level) { - if (typeof (prefix) === "undefined") - prefix = ""; - - if (typeof(level) === "undefined") - level = DEFAULT_LEVEL; - - this._level = level; - this._prefix = prefix; - } - - /** - * Logs a request related information - * @param {string} message - * the request status to log. - * @returns {SieveAbstractLogger} - * a self reference - */ - logRequest(message) { - return this.log(message, LOG_REQUEST); - } - - /** - * Logs response related information - * @param {byte[]} data - * the response status to log - * @returns {SieveAbstractLogger} - * a self reference - */ - logResponse(data) { - const byteArray = new Uint8Array(data.slice(0, data.length)); - - return this.log( - "Server -> Client\n" + (new TextDecoder("UTF-8")).decode(byteArray), - LOG_RESPONSE); - } - - /** - * Logs state machine information. - * @param {string} message - * the stat information to log. - * @returns {SieveAbstractLogger} - * a self reference - */ - logState(message) { - return this.log(message, LOG_STATE); - } - - /** - * Dumps raw stream data to the log - * @param {string} message - * the stream information to log. - * @returns {SieveAbstractLogger} - * a self reference - */ - logStream(message) { - return this.log(message, LOG_STREAM); - } - - /** - * Logs information about the session. - * @param {string} message - * the message to log. - * @returns {SieveAbstractLogger} - * a self reference - */ - logSession(message) { - return this.log(message, LOG_SESSION_INFO); - } - - /** - * Logs the given message to the browser console. - * - * @abstract - * - * @param {string} message - * The message which should be logged - * @param {int} [level] - * the log level. If omitted the message will be always logged. - * @returns {SieveAbstractLogger} - * a self reference - */ - log(message, level) { - throw new Error(`Implement log(${message},${level})`); - } - - /** - * Checks if state information should be logged. - * - * @returns {boolean} - * true in case state information should be logged otherwise false. - */ - isLevelState() { - return this.isLoggable(LOG_STATE); - } - - /** - * Checks if session information should be logged. - * - * @returns {boolean} - * true in case session information should be logged otherwise false. - */ - isLevelSession() { - return this.isLoggable(LOG_SESSION_INFO); - } - - /** - * Tests if the log level should log. - * - * @param {int} level - * the level which should be checked. - * @returns {boolean} - * true in case the log level is activated otherwise false - */ - isLoggable(level) { - if (typeof (level) === "undefined") - return true; - - return !!(this.level() & level); - } - - /** - * Checks if stream data should be logged. - * - * @returns {boolean} - * true in case the stream data should be logged otherwise false - */ - isLevelStream() { - return this.isLoggable(LOG_STREAM); - } - - /** - * Checks if request data should be logged. - * - * @returns {boolean} - * true in case the request data should be logged otherwise false - */ - isLevelRequest() { - return this.isLoggable(LOG_REQUEST); - } - - /** - * Checks if response data should be logged. - * - * @returns {boolean} - * true in case the response data should be logged otherwise false - */ - isLevelResponse() { - return this.isLoggable(LOG_RESPONSE); - } - - /** - * Gets and sets the log level to the given bit mask. - * Note that the log level is a bit mask, every bit in the - * bit mask corresponds to a special logger. - * - * In order to activate or deactivate a logger you need to - * get the level toggle the desired bits and set the new level. - * - * @param {int} [level] - * the desired log level as bit mask. - * @returns {int} - * the current log level - */ - level(level) { - if (typeof (level) !== "undefined") - this._level = level; - - return this._level; - } - - - /** - * Pads the given string with leading zeros - * @private - * - * @param {string} n - * the string which should be padded - * @param {int} m - * the maximum padding. - * - * @returns {string} - * the padded string - */ - _pad(n, m) { - - let str = n; - - for (let i = 0; i < m; i++) - if (n < Math.pow(BASE_10, i)) - str = '0' + str; - - return str; - } - - /** - * Gets the current time in iso format (hh:mm:ss.SSS) - * - * @returns {string} - * the current timestamp as string. - */ - getTimestamp() { - - const date = new Date(); - return this._pad(date.getHours(), TWO_CHARS) - + ":" + this._pad(date.getMinutes(), TWO_CHARS) - + ":" + this._pad(date.getSeconds(), TWO_CHARS) - + "." + this._pad(date.getMilliseconds(), THREE_CHARS); - } - - - /** - * Gets and sets the loggers prefix. The prefix is appended to - * every logger message - * - * @param {string} [prefix] - * the new prefix. - * @returns {string} - * the current prefix. - */ - prefix(prefix) { - - if (typeof (prefix) !== "undefined") - this._prefix = prefix; - - return this._prefix; - } - } - - exports.SieveAbstractLogger = SieveAbstractLogger; - -})(module.exports || this); diff --git a/src/common/libManageSieve/SieveAbstractRequestBuilder.js b/src/common/libManageSieve/SieveAbstractRequestBuilder.js deleted file mode 100755 index 7c56d024..00000000 --- a/src/common/libManageSieve/SieveAbstractRequestBuilder.js +++ /dev/null @@ -1,174 +0,0 @@ -/* - * The contents of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via email - * from the author. Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - // Enable Strict Mode - "use strict"; - - /** - * A helper class used to build standard compliant sieve requests. - */ - class SieveAbstractRequestBuilder { - - /** - * Creates a new instance - */ - constructor() { - this.data = ""; - } - - /** - * Adds a string as quoted base 64 encoded literal to the request. - * - * This is typically needed for SASL requests as they have to be - * base64 encoded by definition. - * - * @param {string} token - * the string which should be added to the request. - * @returns {SieveAbstractRequestBuilder} - * a self reference - */ - addQuotedBase64(token) { - if (token === undefined || token === null) - throw new Error("Invalid token"); - - this.addLiteral('"' + this.convertToBase64(token) + '"'); - return this; - } - - /** - * Adds a string as quoted literal to the request. - * - * This is typically used for string without a line break. - * In case you know you'll have a line break use the multiline - * version for better readability. - * - * Do not use this for any sasl method. All sasl strings - * have to be base 64 encoded. Refer to addQuotedBase64String instead. - * - * @param {string} [token] - * the string which should be added to the request. - * if omitted an empty string is sent. - * @returns {SieveAbstractRequestBuilder} - * a self reference - */ - addQuotedString(token) { - if (typeof (token) === "undefined" || token === null) - token = ""; - - this.addLiteral('"' + this.escapeString(token) + '"'); - return this; - } - - /** - * Adds a string as multiline literal to the request. - * - * It improves the requests readability in case you need to send a - * string containing a line break. - * - * @param {string} token - * the string which should be added to the request. - * @returns {SieveAbstractRequestBuilder} - * a self reference - */ - addMultiLineString(token) { - this.addLiteral('{' + this.calculateByteLength(token) + '+}\r\n' + token); - return this; - } - - /** - * Adds a literal to the request. - * The literal will used as it is. It will not be wrapped in a string or escaped. - * In case you need this use the specialized methods. - * - * @param {string} token - * the literal which should be added. - * @returns {SieveAbstractRequestBuilder} - * a self reference - */ - addLiteral(token) { - - if (this.data !== "") - this.data += " "; - - this.data += token; - return this; - } - - /** - * Returns the current request as it was cached and build up to the call. - * - * @returns {string} - * the current request including a tailing line break - */ - getBytes() { - return this.data + "\r\n"; - } - - /** - * Calculates a strings length in bytes. - * - * UTF uses variable length characters. Which means the length in bytes - * in not necessarily equivalent to the number of characters. - * - * @param {string} data - * the string for which the byte length should be calculated. - * @returns {int} - * the string's length in bytes. - * - * @abstract - */ - calculateByteLength(data) { - throw new Error(`Implement SieveAbstractRequestBuilder::calculateByteLength(${data})`); - } - - /** - * Escapes a string. All Backslashes are converted to \\ while - * all quotes are escaped as \" - * - * @param {string} str - * the string which should be escaped - * @returns {string} - * the escaped string. - */ - escapeString(str) { - return str.replace(/\\/g, "\\\\").replace(/"/g, "\\\""); - } - - /** - * Encodes a string into base64 - * @param {string|byte[]} decoded - * the string or byte array which shall be converted to base64 - * @returns {string} - * the encoded string. - * - * @abstract - */ - convertToBase64(decoded) { - throw new Error(`Implement SieveAbstractRequestBuilder::convertToBase64(${decoded})`); - } - - /** - * Decodes a base64 encoded string - * @param {string} encoded - * the base64 encoded string which should be decoded - * @returns {string} - * the decoded string - * - * @abstract - */ - convertFromBase64(encoded) { - throw new Error(`Implement SieveAbstractRequestBuilder::convertFromBase64(${encoded})`); - } - } - - exports.SieveAbstractRequestBuilder = SieveAbstractRequestBuilder; - -})(module.exports || this); diff --git a/src/common/libManageSieve/SieveAbstractResponseParser.js b/src/common/libManageSieve/SieveAbstractResponseParser.js deleted file mode 100755 index 0ba0c50c..00000000 --- a/src/common/libManageSieve/SieveAbstractResponseParser.js +++ /dev/null @@ -1,463 +0,0 @@ -/* - * The contents of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via email - * from the author. Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - - -(function (exports) { - - // Enable Strict Mode - "use strict"; - - const CHAR_LF = 10; - const CHAR_CR = 13; - const CHAR_SPACE = 32; - const CHAR_QUOTE = 34; - const CHAR_BACKSLASH = 92; - const CHAR_LEFT_BRACES = 123; - const CHAR_RIGHT_BRACES = 125; - - const NOT_FOUND = -1; - const CHAR_LEN = 1; - - /** - * The manage sieve protocol syntax uses a fixed grammar which is based on atomic tokens. - * This class offers an interface to test for and extract these predefined tokens. It supports - * Strings (Quoted and Literal), White Space (Line Break, Space ...) as well as arbitrary tokens. - * - * The parser does not change or alter the byte array's content. So extracting data does not shrink - * the array free any bytes. This parser is just some kind of a view to this array. - * - * Tokens are automatically converted from UTF-8 encoded byte arrays to JavaScript Unicode Strings - * during extraction. - */ - class SieveAbstractResponseParser { - - /** - * Expects as input a byte array using UTF-8 encoding. It's because the manage sieve - * protocol is defined to uses UTF-8 encoding and Mozilla sockets return byte based incoming messages streams. - * @param {byte[]} data - * the response which should be parsed as a byte array encoded in UTF-8 - */ - constructor(data) { - if ((typeof (data) === 'undefined') || (data === null)) - throw new Error("Error Parsing Response...\nData is null"); - - this.pos = 0; - this.data = data; - } - - /** - * Extracts the given number of bytes from the buffer. - * - * @param {int} size - * The number of bytes as integer which should be extracted - * - */ - extract(size) { - this.pos += size; - } - - /** - * Tests if the array starts with a line break (#13#10) - * - * @returns {boolean} - * true if the buffer with a line break, otherwise false - */ - isLineBreak() { - // Are we out of bounds? - if (this.data.length < this.pos + CHAR_LEN) - return false; - - // Test for a line break #13#10 - if (this.data[this.pos] !== CHAR_CR) - return false; - - if (this.data[this.pos + CHAR_LEN] !== CHAR_LF) - return false; - - return true; - } - - /** - * Extracts a line break (#13#10) for the buffer - * - * If it does not start with a line break an exception is thrown. - * - * @returns {SieveAbstractResponseParser} - * a self reference - */ - extractLineBreak() { - if (this.isLineBreak() === false) - throw new Error(`Line break expected but found:\n${this.getData()}`); - - this.pos += "\r\n".length; - - return this; - } - - /** - * Test if the buffer starts with a space character (#32) - * @returns {boolean} - * true if buffer starts with a space character, otherwise false - */ - isSpace() { - if (this.data[this.pos] === CHAR_SPACE) - return true; - - return false; - } - - /** - * Extracts a space character (#32) form the buffer - * - * If it does not start with a space character an exception is thrown. - * - */ - extractSpace() { - if (this.isSpace() === false) - throw new Error(`Space expected but found:\n${this.getData()}`); - - this.pos++; - } - - /** - * Tests if the current buffer position is a literal string. - * Literals strings are defined as: - * - * literal = "{" number "+}" CRLF *OCTET - * - * @returns {boolean} - * true in case it is a literal otherwise false. - */ - isLiteral() { - if (this.data[this.pos] === CHAR_LEFT_BRACES) - return true; - - return false; - } - - /** - * Extracts a literal string from the current position. - * Literals strings are defined as: - * - * literal = "{" number "+}" CRLF *OCTET - * - * Please not it is perfectly fine to have a literal with a zero byte length. - * - * @returns {string} - * the string or an exception in case the literal could not be extracted. - */ - extractLiteral() { - if (this.isLiteral() === false) - throw new Error(`Literal Expected but found\n ${this.getData()}`); - - // remove the "{" - this.pos++; - - // some sieve implementations are broken, this means .... - // ... we can get "{4+}\r\n1234" or "{4}\r\n1234" - - const nextBracket = this.indexOf(CHAR_RIGHT_BRACES); - if (nextBracket === NOT_FOUND) - throw new Error(`Error unbalanced parentheses "{" in \n ${this.getData()}`); - - // extract the size, and ignore "+" - const size = parseInt(this.getData(this.pos, nextBracket).replace(/\+/, ""), 10); - - this.pos = nextBracket + CHAR_LEN; - - this.extractLineBreak(); - - // extract the literal... - const literal = this.getData(this.pos, this.pos + size); - this.pos += size; - - return literal; - } - - /** - * Searches the buffer for a character. - * - * @param {byte} character - * the character which should be found - * @param {int} [offset] - * an absolute offset, from which to start searching - * @returns {int} character - * the characters absolute position within the buffer otherwise -1 if not found - */ - indexOf(character, offset) { - if (typeof (offset) === "undefined") - offset = this.pos; - - for (let i = offset; i < this.data.length; i++) - if (this.data[i] === character) - return i; - - return NOT_FOUND; - } - - /** - * Test if the buffer starts with a quote character (#34) - * @returns {boolean} - * true if buffer starts with a quote character, otherwise false - */ - isQuoted() { - if (this.data[this.pos] === CHAR_QUOTE) - return true; - - return false; - } - - /** - * Extracts a quoted string form the buffer. It is aware of escape sequences. - * - * If it does not start with a valid string an exception is thrown. - * - * @returns {string} - * the quoted string extracted, it is guaranteed to be free of escape sequences - */ - extractQuoted() { - if (this.isQuoted() === false) - throw new Error(`Quoted string expected but found\n${this.getData()}`); - - // now search for the end. But we need to be aware of escape sequences. - let nextQuote = this.pos + CHAR_LEN; - - while (this.data[nextQuote] !== CHAR_QUOTE) { - - // Quoted stings can not contain line breaks... - if (this.data[nextQuote] === CHAR_LF) - throw new Error("Line break (LF) in Quoted String detected"); - - if (this.data[nextQuote] === CHAR_CR) - throw new Error("Line break (CR) in Quoted String detected"); - - // is it an escape sequence? - if (this.data[nextQuote] === CHAR_BACKSLASH) { - // Yes, it's a backslash so get the next char... - nextQuote++; - - // ... only \\ and \" are valid escape sequences - if ((this.data[nextQuote] !== CHAR_BACKSLASH) && (this.data[nextQuote] !== CHAR_QUOTE)) - throw new Error("Invalid Escape Sequence"); - } - - // move to the next character - nextQuote++; - - if (this.nextQuote >= this.data.length) - throw new Error("Unterminated Quoted string"); - } - - let quoted = this.getData(this.pos + CHAR_LEN, nextQuote); - - this.pos = nextQuote + CHAR_LEN; - - // Cleanup escape sequences - quoted = quoted.replace(/\\"/g, '"'); - quoted = quoted.replace(/\\\\/g, '\\'); - - return quoted; - } - - /** - * Tests if the a quoted or literal string starts at the current position. - * - * @returns {boolean} - * true if a strings starts, otherwise false - */ - isString() { - if (this.isQuoted()) - return true; - - if (this.isLiteral()) - return true; - - return false; - } - - /** - * Extracts a quoted or literal string from the current position - * - * @returns {string} - * the quote or literal string or an exception in case no string could be extracted. - */ - extractString() { - if (this.isQuoted()) - return this.extractQuoted(); - if (this.isLiteral()) - return this.extractLiteral(); - - throw new Error(`String expected but found\n${this.getData()}`); - } - - /** - * Extracts a token form a response. The token is being delimited by any - * separator. The extracted token does not include the separator. - * - * Throws an exception if none of the separators is found. - * - * @param {byte[]} separators - * an array containing possible token separators. The first match always wins. - * @returns {string} - * the extracted token. - */ - extractToken(separators) { - // Search for the separators, the one with the lowest index which is not... - // ... equal to -1 wins. The -2 indicates not initialized... - let index = NOT_FOUND; - - for (let i = 0; i < separators.length; i++) { - const idx = this.indexOf(separators[i], this.pos); - - if (idx === NOT_FOUND) - continue; - - if (index === NOT_FOUND) - index = idx; - else - index = Math.min(index, idx); - } - - if (index === NOT_FOUND) - throw new Error(`Delimiter >>${separators}<< not found in: ${this.getData()}`); - - const token = this.getData(this.pos, index); - this.pos = index; - - return token; - } - - /** - * Tests if the buffer starts with the specified bytes. - * - * As the buffer is encoded in UTF-8, the specified bytes have to be - * encoded in UTF-8, otherwise the result is unpredictable. - * - * @param {Byte[]} bytes - * the bytes to compare as byte array encoded in UTF-8 - * - * @returns {boolean} - * true if bytes match the beginning of the buffer, otherwise false - */ - startsWith(bytes) { - if (!bytes.length) - return false; - - for (let i = 0; i < bytes.length; i++) { - let result = false; - - for (let ii = 0; ii < bytes[i].length; ii++) - if (bytes[i][ii] === this.data[this.pos + i]) - result = true; - - if (result === false) - return false; - } - - return true; - } - - /** - * Returns a copy of the current buffer. - * - * @returns {byte[]} - * an a copy of the array's current view. It is encoded in UTF-8 - */ - getByteArray() { - return this.data.slice(this.pos, this.data.length); - } - - /** - * Returns a copy of the response parsers buffer as JavaScript Unicode string. - * - * Manage Sieve encodes literals in UTF-8 while network sockets are usually - * binary. So we can't use java scripts build in string functions as they expect - * pure unicode. - * - * @param {int} [startIndex] - * Optional zero-based index at which to begin. - * @param {int} [endIndex] - * Optional Zero-based index at which to end. - * @returns {string} the copy buffers content - */ - getData(startIndex, endIndex) { - - if (typeof (endIndex) === "undefined" || endIndex === null) - endIndex = this.data.length; - - if (typeof (startIndex) === "undefined" || startIndex === null) - startIndex = this.pos; - - const byteArray = this.data.slice(startIndex, endIndex); - return this.convertToString(byteArray); - } - - - /** - * Check if the buffer is empty. This means the buffer does not contain any - * extractable bytes or tokens. - * - * @returns {boolean} - * true if the buffer is empty, otherwise false - */ - isEmpty() { - if (this.data.length >= this.pos) - return true; - - return false; - } - - /** - * Converts a byte array into an UTF8 encoded string - * - * @param {byte[]} byteArray - * the byte array which should be converted. - * - * @returns {string} - * the UT8 encoded string. - * - * @abstract - */ - convertToString(byteArray) { - throw new Error(`convertToString(${byteArray})`); - } - - /** - * Encodes a clear text string to a base64 encoded string. - * - * @param {string} decoded - * the clear text string which should be encoded. - * @returns {string} - * the base64 encoded string. - * - * @abstract - */ - convertToBase64(decoded) { - throw new Error(`Implement convertToBase64(${decoded})`); - } - - - /** - * Decodes an base64 encoded string into a cleat text string. - * - * @param {string} encoded - * the base64 encoded string which should be decoded. - * @returns {string} - * the decoded string. - * - * @abstract - */ - convertFromBase64(encoded) { - throw new Error(`Implement convertFromBase64(${encoded})`); - } - } - - exports.SieveAbstractResponseParser = SieveAbstractResponseParser; - -})(module.exports || this); diff --git a/src/common/libManageSieve/SieveAbstractSession.js b/src/common/libManageSieve/SieveAbstractSession.js deleted file mode 100644 index b9a0ea12..00000000 --- a/src/common/libManageSieve/SieveAbstractSession.js +++ /dev/null @@ -1,785 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - const { SieveLogger } = require("./SieveLogger.js"); - - const { Sieve } = require("./SieveClient.js"); - - const { - SieveSaslPlainRequest, - SieveSaslLoginRequest, - SieveSaslCramMd5Request, - SieveSaslScramSha1Request, - SieveSaslScramSha256Request, - SieveSaslExternalRequest, - - SieveInitRequest, - SieveListScriptsRequest, - SieveRenameScriptRequest, - SievePutScriptRequest, - SieveDeleteScriptRequest, - SieveGetScriptRequest, - SieveSetActiveRequest, - SieveCheckScriptRequest, - SieveLogoutRequest, - SieveNoopRequest, - SieveCapabilitiesRequest, - SieveStartTLSRequest - } = require("./SieveRequest.js"); - - const { - SieveClientException, - SieveServerException, - SieveReferralException, - SieveTimeOutException, - SieveCertValidationException - } = require("./SieveExceptions.js"); - - const FIRST_ELEMENT = 0; - const SIEVE_PORT = 4190; - - - /** - * This class realizes a manage sieve connection to a remote server. - * It provides the logic for login, logout, heartbeats, watchdogs and - * much more. - * - * It is save to have concurrent call within a session. The sieve backend - * uses a queue to process them. So you don't need to worry about using - * stuff in parallel. - * - * It is highly async but uses the ES6 await syntax, which makes it behave - * like a synchronous api. - */ - class SieveAbstractSession { - - /** - * Creates a new Session instance. - * - * @param {string} id - * the unique session id. - * @param {object.<string, object>} options - * a dictionary with options as key/value pairs. - */ - constructor(id, options) { - this.id = id; - this.options = options; - this.listeners = {}; - this.sieve = null; - } - - /** - * Returns the logger bound to this session. - * @abstract - * - * @returns {SieveLogger} - * a reference to the current logger - */ - getLogger() { - if (!this.logger) - this.logger = new SieveLogger(this.id, this.getOption("logLevel")); - - return this.logger; - } - - /** - * Returns the sieve client bound to this session. - * - * @returns {SieveAbstractClient} - * a reference to the sieve client. - */ - getSieve() { - return this.sieve; - } - - /** - * Creates a new sieve client for this session. - */ - createSieve() { - if (this.sieve !== undefined && this.sieve !== null) - throw new SieveClientException("Sieve Connection Active"); - - this.sieve = new Sieve(this.getLogger()); - } - - /** - * The server may close our connection after being idle for too long. - * This can be prevented by sending regular keep alive packets. - * - * If supported the noop command is used otherwise a capability - * request is used. - */ - async onIdle() { - this.getLogger().logSession("Sending keep alive packet..."); - await this.noop(); - } - - /** - * Binds the capabilities to the sieve object. - * - * @param {object} capabilities - * a struct containing the capabilities. - */ - setCapabilities(capabilities) { - - this.getSieve().setCompatibility(capabilities.getCompatibility()); - - // FIXME we should use a getter and setter... - this.getSieve().capabilities = { - tls: capabilities.getTLS(), - extensions: capabilities.getExtensions(), - sasl: capabilities.getSasl(), - implementation: capabilities.getImplementation(), - version: capabilities.getVersion() - }; - } - - /** - * Checks if the current session is connected to the server - * - * @returns {boolean} - * true in case the session is connected otherwise false. - */ - isConnected() { - if (!this.getSieve()) - return false; - - return this.getSieve().isAlive(); - } - - /** - * Normally the server returns more than one SASL Mechanism. - * The list is sorted by the server. It starts with the most - * preferred mechanism and ends with the least preferred one. - * - * This means in case the user has forced a preferred mechanism. - * We try to use this first. In case is is not supported by the server - * or the user has no preference we start iterating though the advertised - * mechanism until we find a matching one. - * - * The is one exception to this rule. As suggested in the RFC, - * the SASL Login is only used as very last resort. - * - * Note: In case we do not support any of the server's advertised - * mechanism an exception is thrown. - * - * Note: LOGIN is deprecated, it is only used as very last resort. - * - * @param {string} [mechanism] - * the sasl mechanism which shall be used. - * If omitted or set to "default" the most preferred which is supported - * by client ans server is chosen. - * - * @returns {SieveAbstractSaslRequest} - * the sasl request which implements the most preferred compatible mechanism. - */ - getSaslMechanism(mechanism) { - - if (mechanism === undefined || mechanism === null) - mechanism = "default"; - - if (mechanism === "default") - mechanism = [...this.getSieve().capabilities.sasl]; - else - mechanism = [mechanism]; - - // ... translate the SASL Mechanism into an SieveSaslLogin Object ... - while (mechanism.length > 0) { - // remove and test the first element... - switch (mechanism.shift().toUpperCase()) { - case "PLAIN": - return new SieveSaslPlainRequest(); - - case "CRAM-MD5": - return new SieveSaslCramMd5Request(); - - case "SCRAM-SHA-1": - return new SieveSaslScramSha1Request(); - - case "SCRAM-SHA-256": - return new SieveSaslScramSha256Request(); - - case "EXTERNAL": - return new SieveSaslExternalRequest(); - - case "LOGIN": - // as suggested in the RFC, we use SASL LOGIN - // only as last resort... - - // this means in case it is the only mechanism - // we have no options - if (!mechanism.length) - return new SieveSaslLoginRequest(); - - // otherwise be move it to the end of the - // mechanism list. - mechanism.push("LOGIN"); - break; - } - } - - throw new SieveClientException("No compatible SASL Mechanism (error.sasl)"); - } - - /** - * Gets an configuration parameter from the session's options. - * - * @param {string} name - * the option name. - * @param {Function} [fallback] - * the optional fallback value, if not undefined is returned. - * - * @returns {object} - * the option's value. - */ - getOption(name, fallback) { - if (this.options[name] === null || this.options[name] === undefined) - return fallback; - - return this.options[name]; - } - - /** - * Registers the callback listener for the given name. - * There can be at most one listener per name. - * - * To disable a listener just set the callback handler to null - * or undefined - * - * @param {string} name - * the callback event name. - * @param {Function} [callback] - * the callback function, if omitted the handler will be removed. - */ - on(name, callback) { - - if (name === "authenticate") { - this.listeners.onAuthenticate = callback; - return; - } - - if (name === "authorize") { - this.listeners.onAuthorize = callback; - return; - } - - if (name === "proxy") { - this.listeners.onProxyLookup = callback; - return; - } - - if (name === "certerror") { - this.listeners.onCertError = callback; - return; - } - - throw new SieveClientException(`Unknown callback handler ${name}`); - } - - /** - * The authentication starts for secure connections after - * the tls handshake and for unencrypted connection directly - * after the initial server response. - * - * Please note after a successful tls handshake the server - * may update the SASL Mechanism. The server may force the user - * to use TLS by providing initially an empty list of SASL - * Mechanisms. After a successful tls handshake it then upgrades - * the SASL Mechanisms. - */ - async authenticate() { - - const mechanism = this.getOption("sasl", "default"); - - if (mechanism === "none") - return; - - const request = this.getSaslMechanism(mechanism); - - if (!this.listeners.onAuthenticate) - throw new SieveClientException("No Authentication handler registered"); - - const authentication = await this.listeners.onAuthenticate(request.hasPassword()); - - // SASL External has no password it relies completely on SSL... - if (request.hasPassword()) { - const password = authentication.password; - - if (typeof (password) === "undefined" || password === null) - throw new SieveClientException("error.authentication"); - - request.setPassword(password); - } - - request.setUsername(authentication.username); - - // check if the authentication method supports proxy authorization... - if (request.isAuthorizable()) { - - if (!this.listeners.onAuthorize) - throw new SieveClientException("No Authorization handler registered"); - - // ... if so retrieve the authorization identity - const authorization = await this.listeners.onAuthorize(); - - if (typeof (authorization) === "undefined" || authorization === null) - throw new SieveClientException("error.authorization"); - - if (authorization !== "") - request.setAuthorization(authorization); - } - - await this.sendRequest(request, false); - } - - /** - * Starts a new TLS connections. - * It throws an exception in case the tls handshake failed for some reason. - * - * Old Cyrus servers do not advertise their capabilities - * after the tls handshake. So we need some magic here. - * In addition the implicit request we send an explicit - * capability request. - * - * A bug free server will return with two capability responses - * while a buggy implementation returns only one. - * - * @param {object} [options] - * key value pairs with implementation specific options - */ - async startTLS(options) { - - if (!this.getSieve().isSecure()) - return; - - if (!this.getSieve().capabilities.tls) - throw new SieveClientException("Server does not support a secure connection."); - - try { - await this.sendRequest(new SieveStartTLSRequest(), false); - - await this.getSieve().startTLS(options); - - // A bug free server we end up with two capability request, one - // implicit after startTLS and one explicit from capabilities. - // So we have to consume one of them silently... - const capabilities = await this.sendRequest([ - new SieveCapabilitiesRequest(), - new SieveInitRequest()], false); - - this.setCapabilities(capabilities); - - } catch (ex) { - // Upon a cert validation we emit a notification... - if (ex instanceof SieveCertValidationException) - if (this.listeners.onCertError) - this.listeners.onCertError(ex.getSecurityInfo()); - - // ... and then rethrow. - throw ex; - } - } - - /** - * Converts a callback driven request into async/await code. - * - * @param {SieveAbstractRequest|Array<SieveAbstractRequest>} request - * a request or list of request to be executed. They will be queued - * at the same time and only the first request is used to resolve - * the promise. - * - * @param {Function} [init] - * an optional init function which will be called directly after - * the first request was queued - * - * @returns {SieveAbstractResponse} - * the response for the first request or an exception in case of an error. - */ - async promisify(request, init) { - - if (!Array.isArray(request)) - request = [request]; - - return await new Promise((resolve, reject) => { - - request[FIRST_ELEMENT].addResponseListener((response) => { - resolve(response); - }); - - request[FIRST_ELEMENT].addErrorListener((response) => { - reject(new SieveServerException(response)); - }); - - request[FIRST_ELEMENT].addByeListener((response) => { - - if (response.getResponseCode().equalsCode("REFERRAL")) { - reject(new SieveReferralException(response)); - return; - } - - reject(new SieveServerException(response)); - }); - - request[FIRST_ELEMENT].addTimeoutListener((error) => { - - if (error) { - reject(error); - return; - } - - reject(new SieveTimeOutException("Request was canceled or took too long")); - }); - - - this.getSieve().addRequest(request[FIRST_ELEMENT]); - - if (init) - init(); - - while (request.length > 1) { - request.shift(); - this.getSieve().addRequest(request[FIRST_ELEMENT], true); - } - }); - } - - /** - * Converts a callback driven request into async/await code. - * - * @param {SieveAbstractRequest|Array<SieveAbstractRequest>} request - * a request or list of request to be executed. They will be queued - * at the same time and only the first request is used to resolve - * the promise. - * - * @param {boolean} [follow] - * If true or omitted the request should follow server side referrals. - * In case the server responded with a bye and a referral url. The request - * will be queued into the new connections request queue. - * - * @param {Function} [init] - * an optional init function which will be called directly after - * the first request was queued - * - * @returns {SieveAbstractResponse} - * the response for the first request or an exception in case of an error. - */ - async sendRequest(request, follow, init) { - - if (follow === undefined || follow === null) - follow = true; - - try { - return await this.promisify(request, init); - } - catch (ex) { - - if (!(ex instanceof SieveReferralException)) { - this.getLogger().logSession(`Sending Request failed ${ex}`); - throw ex; - } - - if (!follow) { - this.getLogger().logSession(`Sending Request failed ${ex}`); - throw ex; - } - - await this.disconnect(true); - - this.getLogger().logSession(`Referred to Server: ${ex.getHostname()}`); - await this.connect(ex.getHostname(), ex.getPort()); - - return await this.promisify(request, init); - } - } - - - /** - * An internal method creating a server connection. - * - * @param {string} hostname - * the sieve server's hostname. - * @param {string} [port] - * the sieve server's port. If omitted the default port 4190 is used. - * @returns {SieveSession} - * a self reference - */ - async connect(hostname, port) { - - if (typeof (hostname) === "undefined" || hostname === null) - throw new SieveClientException("No Hostname specified"); - - if (typeof (port) === "undefined" || port === null) - port = SIEVE_PORT; - - this.createSieve(); - - this.getSieve().setIdleWait(this.getOption("keepAlive")); - - // TODO do we really need this? Or do we need this only for keep alive? - this.getSieve().addListener(this); - - let proxy = null; - if (this.listeners.onProxyLookup) - proxy = await this.listeners.onProxyLookup(hostname, port); - - const init = () => { - this.getSieve().connect( - hostname, port, - this.getOption("secure", true), - this, - proxy); - }; - - this.setCapabilities( - await this.sendRequest(new SieveInitRequest(), false, init)); - - await this.startTLS(); - - await this.authenticate(); - - return this; - } - - /** - * Disconnects the current sieve session. - * - * The disconnect is by default graceful, which means the client send a - * logout command and waits for the server to terminate the connection. - * - * @param {boolean} [force] - * if set to true the disconnect will be forced and not graceful. - * This means the connection will be just disconnected. - * @returns {SieveSession} - * a self reference. - */ - async disconnect(force) { - - if (this.getSieve() === null) - return this; - - // We try to exit with a graceful Logout request... - if (!force && this.getSieve().isAlive()) { - try { - await this.logout(); - } catch (ex) { - this.getLogger().logSession(`Graceful logout failed ${ex}`); - } - } - - // ... in case it failed for we do it the hard way - if (this.getSieve()) { - await this.getSieve().disconnect(); - this.sieve = null; - } - - return this; - } - - /** - * Lists all scripts available on the server. - * @returns {string} - * the current scripts. - */ - async listScripts() { - return (await this.sendRequest(new SieveListScriptsRequest())).getScripts(); - } - - /** - * Renames a script. - * - * It prefers the new rename command. In case it is not supported it will - * use a get, put and delete sequence to emulate the rename command. - * - * So the result will be the very same, but there is one slight difference. - * Instead of throwing an error it will overwrite existing script with the - * same name silently. - * - * @param {string} oldName - * the old name - * @param {string} newName - * the new name - */ - async renameScript(oldName, newName) { - - if (this.getSieve().getCompatibility().renamescript) { - await this.sendRequest(new SieveRenameScriptRequest(oldName, newName)); - return; - } - - // Get the scripts activation state and check if the script name clashes - const scripts = await this.listScripts(); - let active = null; - - for (const item of scripts) { - if (item.script === newName) - throw new SieveClientException("Name already exists"); - - if (item.script === oldName) - active = item.active; - } - - if (active === null) - throw new SieveClientException(`Unknown Script ${oldName}`); - - // Get the script'S content and save is as a new file - await this.putScript(newName, - await this.getScript(oldName)); - - // Activate the new script - if (this.active === true) - await this.activateScript(newName); - - // Finally delete the old script - await this.deleteScript(oldName); - } - - /** - * Saves the given script. In case the script exists - * it will be silently overwritten. - * - * @param {string} name - * the script's name - * @param {string} script - * the script which should be saved. - * - */ - async putScript(name, script) { - await this.sendRequest(new SievePutScriptRequest(name, script)); - } - - /** - * Deletes the given script name. - * - * @param {string} name - * the script which should be deleted. - * - */ - async deleteScript(name) { - await this.sendRequest(new SieveDeleteScriptRequest(name)); - } - - /** - * Gets the script with the given name. - * In case the script does not exists the server will throw an error. - * - * @param {string} name - * the scripts unique name - * @returns {Promise<string>} - * the scripts content as string - */ - async getScript(name) { - return (await this.sendRequest(new SieveGetScriptRequest(name))).getScriptBody(); - } - - /** - * Activates the specified script and deactivated the current script. - * Sieve supports at most one active script. - * - * To deactivate all scripts just omit the script parameter - * - * @param {string} [script] - * the script which should be activated. - * If omitted all script will be deactivated. - * - */ - async activateScript(script) { - await this.sendRequest(new SieveSetActiveRequest(script)); - } - - /** - * Checks the script for syntax errors. - * - * It uses the checkscript command if present otherwise - * it emulates the checkscript by pushing a temporary script - * to the server. - * - * Throws an exception in case the script is not valid. - * - * @param {string} script - * the script which should be checked. - * - */ - async checkScript(script) { - - // We do not need to check an empty script... - if (!script.length) - return; - - // Use the CHECKSCRIPT command when possible, otherwise we need to ... - // ... fallback to the PUTSCRIPT/DELETESCRIPT Hack... - if (this.getSieve().getCompatibility().checkscript) { - await this.sendRequest(new SieveCheckScriptRequest(script)); - return; - } - - // ... we have to use the PUTSCRIPT/DELETESCRIPT Hack. - - // First we use PUTSCRIPT to store a temporary script on the server... - // ... incase the command fails, it is most likely due to an syntax error... - // ... if it succeeds the script is syntactically correct! - await this.putScript("TMP_FILE_DELETE_ME", script); - - // then delete the temporary script. We need to do this only when - // put script succeeded and when it was stored. - await this.deleteScript("TMP_FILE_DELETE_ME"); - } - - /** - * Sends a noop or keep alive response. - * It is a request without side effect and without any - * payload. It is typically used to test if the server - * is available and to prevent closing the connection to the server. - * - * Servers do not have to support noop. - * The implementation will use a capability request as - * fallback as described in the rfc. - * - */ - async noop() { - - // In case th server does not support noop we fallback - // to a capability request as suggested in the rfc. - if (!this.getSieve().getCompatibility().noop) { - await this.capabilities(); - } - - await this.sendRequest(new SieveNoopRequest()); - } - - /** - * Sends a capability request. - * In case of an error an exception will be thrown. - * - * @returns {object} - * an object with the capabilities. - */ - async capabilities() { - return (await this.sendRequest(new SieveCapabilitiesRequest())).getDetails(); - } - - /** - * Used to gracefully disconnect from the server. - * It sends a logout request, then the server should - * hangup the connection. - */ - async logout() { - await this.sendRequest(new SieveLogoutRequest(), false); - } - - } - - exports.SieveAbstractSession = SieveAbstractSession; - -})(module.exports || this); diff --git a/src/common/libManageSieve/SieveAbstractSession.mjs b/src/common/libManageSieve/SieveAbstractSession.mjs new file mode 100644 index 00000000..4368c8e6 --- /dev/null +++ b/src/common/libManageSieve/SieveAbstractSession.mjs @@ -0,0 +1,841 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +import { SieveLogger } from "./SieveLogger.mjs"; +import { Sieve } from "./SieveClient.mjs"; + +import { + SieveSaslPlainRequest, + SieveSaslLoginRequest, + SieveSaslScramSha1Request, + SieveSaslScramSha256Request, + SieveSaslScramSha512Request, + SieveSaslExternalRequest, + + SieveInitRequest, + SieveListScriptsRequest, + SieveRenameScriptRequest, + SievePutScriptRequest, + SieveDeleteScriptRequest, + SieveGetScriptRequest, + SieveSetActiveRequest, + SieveCheckScriptRequest, + SieveLogoutRequest, + SieveNoopRequest, + SieveCapabilitiesRequest, + SieveStartTLSRequest +} from "./SieveRequest.mjs"; + +import { + SieveClientException, + SieveServerException, + SieveReferralException, + SieveTimeOutException +} from "./SieveExceptions.mjs"; + +const SIEVE_PORT = 4190; + + +/** + * This class realizes a manage sieve connection to a remote server. + * It provides the logic for login, logout, heartbeats, watchdogs and + * much more. + * + * It is save to have concurrent call within a session. The sieve backend + * uses a queue to process them. So you don't need to worry about using + * stuff in parallel. + * + * It is highly async but uses the ES6 await syntax, which makes it behave + * like a synchronous api. + */ +class SieveAbstractSession { + + /** + * Creates a new Session instance. + * + * @param {string} id + * the unique session id. + * @param {object.<string, object>} options + * a dictionary with options as key/value pairs. + */ + constructor(id, options) { + this.id = id; + this.options = options; + this.listeners = {}; + this.sieve = null; + } + + /** + * Returns the logger bound to this session. + * @abstract + * + * @returns {SieveLogger} + * a reference to the current logger + */ + getLogger() { + if (!this.logger) + this.logger = new SieveLogger(this.id, this.getOption("logLevel")); + + return this.logger; + } + + /** + * Returns the sieve client bound to this session. + * + * @returns {SieveAbstractClient} + * a reference to the sieve client. + */ + getSieve() { + return this.sieve; + } + + /** + * Creates a new sieve client for this session. + */ + createSieve() { + if (typeof(this.sieve) !== "undefined" && this.sieve !== null) + throw new SieveClientException("Sieve Connection Active"); + + this.sieve = new Sieve(this.getLogger()); + } + + /** + * The server may close our connection after being idle for too long. + * This can be prevented by sending regular keep alive packets. + * + * If supported the noop command is used otherwise a capability + * request is used. + */ + async onIdle() { + this.getLogger().logSession("Sending keep alive packet..."); + await this.noop(); + } + + /** + * The default error handler called upon any unhandled error or exception. + * Called e.g. when the connection to the server was terminated unexpectedly. + * + * The default behaviour is to disconnect. + * + * @param {Error} error + * the error message which causes this exceptional state. + */ + async onError(error) { + this.getLogger().logSession(`SieveAbstractSession OnError: ${error.message}`); + await this.disconnect(true); + } + + /** + * Called when the connection gets disconnected by the server. + */ + async onDisconnected() { + this.getLogger().logSession(`SieveAbstractSession: onDisconnected`); + + // TODO Do we really need this? + await this.disconnect(true); + } + + /** + * Binds the capabilities to the sieve object. + * + * @param {object} capabilities + * a struct containing the capabilities. + */ + setCapabilities(capabilities) { + + this.getSieve().setCompatibility(capabilities.getCompatibility()); + + // FIXME we should use a getter and setter... + this.getSieve().capabilities = { + tls: capabilities.getTLS(), + extensions: capabilities.getExtensions(), + sasl: capabilities.getSasl(), + implementation: capabilities.getImplementation(), + version: capabilities.getVersion() + }; + } + + /** + * Checks if the current session is connected to the server + * + * @returns {boolean} + * true in case the session is connected otherwise false. + */ + isConnected() { + if (!this.getSieve()) + return false; + + return this.getSieve().isAlive(); + } + + /** + * Normally the server returns more than one SASL Mechanism. + * The list is sorted by the server. It starts with the most + * preferred mechanism and ends with the least preferred one. + * + * This means in case the user has forced a preferred mechanism. + * We try to use this first. In case is is not supported by the server + * or the user has no preference we start iterating though the advertised + * mechanism until we find a matching one. + * + * The is one exception to this rule. As suggested in the RFC, + * the SASL Login is only used as very last resort. + * + * Note: In case we do not support any of the server's advertised + * mechanism an exception is thrown. + * + * Note: LOGIN is deprecated, it is only used as very last resort. + * + * @param {string} [mechanism] + * the sasl mechanism which shall be used. + * If omitted or set to "default" the most preferred which is supported + * by client ans server is chosen. + * + * @returns {SieveAbstractSaslRequest} + * the sasl request which implements the most preferred compatible mechanism. + */ + getSaslMechanism(mechanism) { + + if (mechanism === undefined || mechanism === null) + mechanism = "default"; + + if (mechanism === "default") + mechanism = [...this.getSieve().capabilities.sasl]; + else + mechanism = [mechanism]; + + // ... translate the SASL Mechanism into an SieveSaslLogin Object ... + while (mechanism.length > 0) { + // remove and test the first element... + switch (mechanism.shift().toUpperCase()) { + case "PLAIN": + return new SieveSaslPlainRequest(); + + case "SCRAM-SHA-1": + return new SieveSaslScramSha1Request(); + + case "SCRAM-SHA-256": + return new SieveSaslScramSha256Request(); + + case "SCRAM-SHA-512": + return new SieveSaslScramSha512Request(); + + case "EXTERNAL": + return new SieveSaslExternalRequest(); + + case "LOGIN": + // as suggested in the RFC, we use SASL LOGIN + // only as last resort... + + // this means in case it is the only mechanism + // we have no options + if (!mechanism.length) + return new SieveSaslLoginRequest(); + + // otherwise be move it to the end of the + // mechanism list. + mechanism.push("LOGIN"); + break; + } + } + + throw new SieveClientException("No compatible SASL Mechanism (error.sasl)"); + } + + /** + * Gets an configuration parameter from the session's options. + * + * @param {string} name + * the option name. + * @param {Function} [fallback] + * the optional fallback value, if not undefined is returned. + * + * @returns {object} + * the option's value. + */ + getOption(name, fallback) { + if (this.options[name] === null || this.options[name] === undefined) + return fallback; + + return this.options[name]; + } + + /** + * Registers the callback listener for the given name. + * There can be at most one listener per name. + * + * To disable a listener just set the callback handler to null + * or undefined + * + * @param {string} name + * the callback event name. + * @param {Function} [callback] + * the callback function, if omitted the handler will be removed. + */ + on(name, callback) { + + if (name === "authenticate") { + this.listeners.onAuthenticate = callback; + return; + } + + if (name === "authorize") { + this.listeners.onAuthorize = callback; + return; + } + + if (name === "error") { + this.listeners.onError = callback; + return; + } + + if (name === "disconnected") { + this.listeners.onDisconnected = callback; + return; + } + + throw new SieveClientException(`Unknown callback handler ${name}`); + } + + /** + * The authentication starts for secure connections after + * the tls handshake and for unencrypted connection directly + * after the initial server response. + * + * Please note after a successful tls handshake the server + * may update the SASL Mechanism. The server may force the user + * to use TLS by providing initially an empty list of SASL + * Mechanisms. After a successful tls handshake it then upgrades + * the SASL Mechanisms. + */ + async authenticate() { + + const mechanism = this.getOption("sasl", "default"); + + if (mechanism === "none") + return; + + const request = this.getSaslMechanism(mechanism); + + if (!this.listeners.onAuthenticate) + throw new SieveClientException("No Authentication handler registered"); + + const authentication = await this.listeners.onAuthenticate(request.hasPassword()); + + // SASL External has no password it relies completely on SSL... + if (request.hasPassword()) { + const password = authentication.password; + + if (typeof (password) === "undefined" || password === null) + throw new SieveClientException("error.authentication"); + + request.setPassword(password); + } + + request.setUsername(authentication.username); + + // check if the authentication method supports proxy authorization... + if (request.isAuthorizable()) { + + if (!this.listeners.onAuthorize) + throw new SieveClientException("No Authorization handler registered"); + + // ... if so retrieve the authorization identity + const authorization = await this.listeners.onAuthorize(); + + if (typeof (authorization) === "undefined" || authorization === null) + throw new SieveClientException("error.authorization"); + + if (authorization !== "") + request.setAuthorization(authorization); + } + + await this.sendRequest(request); + } + + /** + * Starts a new TLS connections. + * It throws an exception in case the tls handshake failed for some reason. + * + * Old Cyrus servers do not advertise their capabilities + * after the tls handshake. So we need some magic here. + * In addition the implicit request we send an explicit + * capability request. + * + * A bug free server will return with two capability responses + * while a buggy implementation returns only one. + * + * @param {object} [options] + * key value pairs with implementation specific options + */ + async startTLS(options) { + + if (!this.getSieve().isSecure()) + return; + + if (!this.getSieve().capabilities.tls) + throw new SieveClientException("Server does not support a secure connection."); + + await this.sendRequest(new SieveStartTLSRequest()); + + await this.getSieve().startTLS(options); + + // After a successfully tls handshake the server will advertise + // the capabilities, especially the SASL mechanism are likely to change. + + // Old Cyrus implementation fail to advertise the capabilities. + // So that we actively request them as optional. This does not + // hurt bug free implementations and makes cyrus happy. + const capabilities = await this.sendRequest(new SieveCapabilitiesRequest()); + this.sendRequest(new SieveInitRequest().makeOptional()); + + this.setCapabilities(capabilities); + } + + /** + * Converts a callback driven request into async/await code. + * + * @param {SieveAbstractRequest} request + * a request or list of request to be executed. They will be queued + * at the same time and only the first request is used to resolve + * the promise. + * + * @param {Function} [init] + * an optional init function which will be called directly after + * the first request was queued + * + * @returns {SieveAbstractResponse} + * the response for the first request or an exception in case of an error. + */ + async promisify(request, init) { + + // eslint-disable-next-line no-async-promise-executor + return await new Promise(async (resolve, reject) => { + + request.addResponseListener((response) => { + resolve(response); + }); + + request.addErrorListener((response) => { + reject(new SieveServerException(response)); + }); + + request.addByeListener((response) => { + + if (response.getResponseCode().equalsCode("REFERRAL")) { + reject(new SieveReferralException(response)); + return; + } + + reject(new SieveServerException(response)); + }); + + request.addTimeoutListener((error) => { + + if (error) { + reject(error); + return; + } + + reject(new SieveTimeOutException("Request was canceled or took too long")); + }); + + + await (this.getSieve().addRequest(request)); + + if (init) + init(); + }); + } + + /** + * Refers the connection to a different server. + * + * The server can send a referral request to the client at any time. + * The client's job is to disconnects and reconnect to the referred + * server's hostname and port. + * + * @param {string} host + * the new hostname + * @param {int} port + * the new hostname's port + * + * @returns {SieveAbstractSession} + * the response for the first request or an exception in case of an error. + */ + async refer(host, port) { + this.getLogger().logSession(`SieveAbstractSession: Disconnecting old connection`); + await this.disconnect(true); + + this.getLogger().logSession(`SieveAbstractSession: Connecting to referred Server: ${host}:${port}`); + return await this.connect(host, port); + } + + /** + * Converts a callback driven request into async/await code. + * + * @param {SieveAbstractRequest|Array<SieveAbstractRequest>} request + * a request or list of request to be executed. They will be queued + * at the same time and only the first request is used to resolve + * the promise. + * + * @param {Function} [init] + * an optional init function which will be called directly after + * the first request was queued + * + * @returns {SieveAbstractResponse} + * the response for the first request or an exception in case of an error. + */ + async sendRequest(request, init) { + + try { + return await this.promisify(request, init); + } + catch (ex) { + + if ((ex instanceof SieveReferralException) && (this.canRefer)) { + this.getLogger().logSession(`Referral received`); + this.getLogger().logSession(`Switching to ${ex.getHostname()}:${ex.getPort()}`); + await this.refer(ex.getHostname(), ex.getPort()); + return await this.promisify(request, init); + } + + this.getLogger().logSession(`Sending Request failed ${ex}`); + throw ex; + } + } + + /** + * By default a request will follow automatically a referral. + * This means it will transparently disconnect from the old + * and reconnect to the new host. + * + * In case the following is disabled an exception will be thrown + * instead. + * + * You want to disable the implicit referral during stateful phases + * like initial connection. + * + * Imagine the following scenario. The connection is secured via + * startTLS and during authentication the referral is received. + * Then the automatic referral would then disconnect from the old + * host and connect to the new host. Then it would try to continue + * with the authentication step on a non secure connection. The startTLS + * step simply got lost. + */ + disableReferrals() { + this.canRefer = false; + } + + /** + * Enables automatic referral following. + * + * see the disableReferrals method for more details. + */ + enableReferrals() { + this.canRefer = true; + } + + + /** + * An internal method creating a server connection. + * + * @param {string} hostname + * the sieve server's hostname. + * @param {string} [port] + * the sieve server's port. If omitted the default port 4190 is used. + * @returns {SieveSession} + * a self reference + */ + async connect(hostname, port) { + + if (typeof (hostname) === "undefined" || hostname === null) + throw new SieveClientException("No Hostname specified"); + + if (typeof (port) === "undefined" || port === null) + port = SIEVE_PORT; + + this.createSieve(); + + this.getSieve().setIdleWait(this.getOption("keepAlive")); + + // TODO do we really need this? Or do we need this only for keep alive? + this.getSieve().addListener(this); + + // A referral during connection means we need to connect to the new + // server and start the whole handshake process again. + this.disableReferrals(); + + try { + + const init = () => { + this.getSieve().connect( + hostname, port, + this.getOption("secure", true), + this, + null); + }; + + this.setCapabilities( + await this.sendRequest(new SieveInitRequest(), init)); + + await this.startTLS(); + + await this.authenticate(); + } catch (ex) { + + if (!(ex instanceof SieveReferralException)) + throw ex; + + // In case we got a referral we renegotiate the whole authentication + this.getLogger().logSession(`Referral received during authentication`); + this.getLogger().logSession(`Switching to ${ex.getHostname()}:${ex.getPort()}`); + + await this.refer(ex.getHostname(), ex.getPort()); + } finally { + this.enableReferrals(true); + } + + return this; + } + + /** + * Disconnects the current sieve session. + * + * The disconnect is by default graceful, which means the client send a + * logout command and waits for the server to terminate the connection. + * + * @param {boolean} [force] + * if set to true the disconnect will be forced and not graceful. + * This means the connection will be just disconnected. + * @returns {SieveSession} + * a self reference. + */ + async disconnect(force) { + + if (this.getSieve() === null) + return this; + + this.getLogger().logSession(`SieveAbstractSession: Disconnecting Session ${force}`); + + // We try to exit with a graceful Logout request... + if (!force && this.getSieve().isAlive()) { + try { + await this.logout(); + } catch (ex) { + this.getLogger().logSession(`Graceful logout failed ${ex}`); + } + } + + // ... in case it failed for we do it the hard way + if (this.getSieve()) { + this.getLogger().logSession(`SieveAbstractSession: Forcing Disconnect`); + await this.getSieve().disconnect(); + this.sieve = null; + } + + return this; + } + + /** + * Lists all scripts available on the server. + * @returns {string} + * the current scripts. + */ + async listScripts() { + return (await this.sendRequest(new SieveListScriptsRequest())).getScripts(); + } + + /** + * Renames a script. + * + * It prefers the new rename command. In case it is not supported it will + * use a get, put and delete sequence to emulate the rename command. + * + * So the result will be the very same, but there is one slight difference. + * Instead of throwing an error it will overwrite existing script with the + * same name silently. + * + * @param {string} oldName + * the old name + * @param {string} newName + * the new name + */ + async renameScript(oldName, newName) { + + if (this.getSieve().getCompatibility().renamescript) { + await this.sendRequest(new SieveRenameScriptRequest(oldName, newName)); + return; + } + + // Get the scripts activation state and check if the script name clashes + const scripts = await this.listScripts(); + let active = null; + + for (const item of scripts) { + if (item.script === newName) + throw new SieveClientException("Name already exists"); + + if (item.script === oldName) + active = item.active; + } + + if (active === null) + throw new SieveClientException(`Unknown Script ${oldName}`); + + // Get the script'S content and save is as a new file + await this.putScript(newName, + await this.getScript(oldName)); + + // Activate the new script + if (this.active === true) + await this.activateScript(newName); + + // Finally delete the old script + await this.deleteScript(oldName); + } + + /** + * Saves the given script. In case the script exists + * it will be silently overwritten. + * + * @param {string} name + * the script's name + * @param {string} script + * the script which should be saved. + * + */ + async putScript(name, script) { + await this.sendRequest(new SievePutScriptRequest(name, script)); + } + + /** + * Deletes the given script name. + * + * @param {string} name + * the script which should be deleted. + * + */ + async deleteScript(name) { + await this.sendRequest(new SieveDeleteScriptRequest(name)); + } + + /** + * Gets the script with the given name. + * In case the script does not exists the server will throw an error. + * + * @param {string} name + * the scripts unique name + * @returns {Promise<string>} + * the scripts content as string + */ + async getScript(name) { + return (await this.sendRequest(new SieveGetScriptRequest(name))).getScriptBody(); + } + + /** + * Activates the specified script and deactivated the current script. + * Sieve supports at most one active script. + * + * To deactivate all scripts just omit the script parameter + * + * @param {string} [script] + * the script which should be activated. + * If omitted all script will be deactivated. + * + */ + async activateScript(script) { + await this.sendRequest(new SieveSetActiveRequest(script)); + } + + /** + * Checks the script for syntax errors. + * + * It uses the checkscript command if present otherwise + * it emulates the checkscript by pushing a temporary script + * to the server. + * + * Throws an exception in case the script is not valid. + * + * @param {string} script + * the script which should be checked. + * + */ + async checkScript(script) { + + // We do not need to check an empty script... + if (!script.length) + return; + + // Use the CHECKSCRIPT command when possible, otherwise we need to ... + // ... fallback to the PUTSCRIPT/DELETESCRIPT Hack... + if (this.getSieve().getCompatibility().checkscript) { + await this.sendRequest(new SieveCheckScriptRequest(script)); + return; + } + + // ... we have to use the PUTSCRIPT/DELETESCRIPT Hack. + + // First we use PUTSCRIPT to store a temporary script on the server... + // ... incase the command fails, it is most likely due to an syntax error... + // ... if it succeeds the script is syntactically correct! + await this.putScript("TMP_FILE_DELETE_ME", script); + + // then delete the temporary script. We need to do this only when + // put script succeeded and when it was stored. + await this.deleteScript("TMP_FILE_DELETE_ME"); + } + + /** + * Sends a noop or keep alive response. + * It is a request without side effect and without any + * payload. It is typically used to test if the server + * is available and to prevent closing the connection to the server. + * + * Servers do not have to support noop. + * The implementation will use a capability request as + * fallback as described in the rfc. + * + */ + async noop() { + + // In case th server does not support noop we fallback + // to a capability request as suggested in the rfc. + if (!this.getSieve().getCompatibility().noop) { + await this.capabilities(); + } + + await this.sendRequest(new SieveNoopRequest()); + } + + /** + * Sends a capability request. + * In case of an error an exception will be thrown. + * + * @returns {object} + * an object with the capabilities. + */ + async capabilities() { + return (await this.sendRequest(new SieveCapabilitiesRequest())).getDetails(); + } + + /** + * Used to gracefully disconnect from the server. + * It sends a logout request, then the server should + * hangup the connection. + */ + async logout() { + await this.sendRequest(new SieveLogoutRequest()); + } + +} + +export { SieveAbstractSession }; diff --git a/src/common/libManageSieve/SieveAbstractTimer.mjs b/src/common/libManageSieve/SieveAbstractTimer.mjs new file mode 100644 index 00000000..fd14645c --- /dev/null +++ b/src/common/libManageSieve/SieveAbstractTimer.mjs @@ -0,0 +1,38 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +/** + * Implements an abstract interface for a simple single shot timer. + */ +class SieveAbstractTimer { + + /** + * Starts the timer. In case the timer is already running it will be restarted + * @abstract + * + * @param {Function} callback + * the callback to be invoked when the timer fires. + * @param {int} ms + * the ms after which the timer should fire + */ + start(callback, ms) { + throw new Error(`Override SieveAbstractTimer::start(${callback},${ms})`); + } + + /** + * Stops the timer. It will fail silently in case the timer is already stopped. + */ + cancel() { + throw new Error(`Override SieveAbstractTimer::cancel()`); + } +} + +export { SieveAbstractTimer }; diff --git a/src/common/libManageSieve/SieveExceptions.js b/src/common/libManageSieve/SieveExceptions.js deleted file mode 100644 index 2aeecc8a..00000000 --- a/src/common/libManageSieve/SieveExceptions.js +++ /dev/null @@ -1,137 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - /** - * A generic base class for Sieve Exceptions. - */ - class SieveException extends Error { - } - - /** - * An error on the client side. - */ - class SieveClientException extends SieveException { - } - - /** - * The request took too long or it was canceled. - * This is a clients side exception. - */ - class SieveTimeOutException extends SieveClientException { - - /** - * Creates a new timeout exception. - * - * @param {Error} [error] - * the optional root error which caused this exception. - */ - constructor(error) { - super("Request took too long or was canceled."); - this.error = error; - } - } - - /** - * The Certificate validation failed. - */ - class SieveCertValidationException extends SieveClientException { - /** - * Creates a Certificate Validation Exception. - * - * @param {object} securityInfo - * the security info object with details on the certificate. - */ - constructor(securityInfo) { - super("Error while validating Certificate"); - - this.securityInfo = securityInfo; - } - - /** - * The security Info object with detailed information - * on the certificate which caused this error. - * - * @returns {object} - * the security info. - */ - getSecurityInfo() { - return this.securityInfo; - } - } - - /** - * The server signaled an error. - * - * The most reliable way to recover from such an error is to - * disconnect and then reconnect to the server. - */ - class SieveServerException extends SieveException { - - /** - * Creates a server side exception - * - * @param {SieveSimpleResponse} response - * the servers response which indicated the error. - */ - constructor(response) { - super(response.getMessage()); - this.response = response; - } - - /** - * Returns the server's response it typically contains the cause - * why the request failed. - * - * @returns {SieveSimpleResponse} - * the server response objet - */ - getResponse() { - return this.response; - } - } - - /** - * The server terminated the connection and referred to a new host - */ - class SieveReferralException extends SieveServerException { - - /** - * The new remote hostname to which the server referred the connection. - * @returns {string} - * the hostname - */ - getHostname() { - return this.getResponse().getResponseCode().getHostname(); - } - - /** - * The new remote port to which the server referred the connection. - * @returns {string} - * the port - */ - getPort() { - return this.getResponse().getResponseCode().getPort(); - } - } - - exports.SieveClientException = SieveClientException; - exports.SieveReferralException = SieveReferralException; - exports.SieveServerException = SieveServerException; - exports.SieveTimeOutException = SieveTimeOutException; - exports.SieveCertValidationException = SieveCertValidationException; - - exports.SieveException = SieveException; - -})(module.exports || this); diff --git a/src/common/libManageSieve/SieveExceptions.mjs b/src/common/libManageSieve/SieveExceptions.mjs new file mode 100644 index 00000000..cb59a73f --- /dev/null +++ b/src/common/libManageSieve/SieveExceptions.mjs @@ -0,0 +1,133 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + + +/** + * A generic base class for Sieve Exceptions. + */ +class SieveException extends Error { +} + +/** + * An error on the client side. + */ +class SieveClientException extends SieveException { +} + +/** + * The request took too long or it was canceled. + * This is a clients side exception. + */ +class SieveTimeOutException extends SieveClientException { + + /** + * Creates a new timeout exception. + * + * @param {Error} [error] + * the optional root error which caused this exception. + */ + constructor(error) { + super("Request took too long or was canceled."); + this.error = error; + } +} + +/** + * The Certificate validation failed. + */ +class SieveCertValidationException extends SieveClientException { + /** + * Creates a Certificate Validation Exception. + * + * @param {object} securityInfo + * the security info object with details on the certificate. + */ + constructor(securityInfo) { + super("Error while validating Certificate"); + + this.securityInfo = securityInfo; + } + + /** + * The security Info object with detailed information + * on the certificate which caused this error. + * + * @returns {object} + * the security info. + */ + getSecurityInfo() { + return this.securityInfo; + } +} + +/** + * The server signaled an error. + * + * The most reliable way to recover from such an error is to + * disconnect and then reconnect to the server. + */ +class SieveServerException extends SieveException { + + /** + * Creates a server side exception + * + * @param {SieveSimpleResponse} response + * the servers response which indicated the error. + */ + constructor(response) { + super(response.getMessage()); + this.response = response; + } + + /** + * Returns the server's response it typically contains the cause + * why the request failed. + * + * @returns {SieveSimpleResponse} + * the server response objet + */ + getResponse() { + return this.response; + } +} + +/** + * The server terminated the connection and referred to a new host + */ +class SieveReferralException extends SieveServerException { + + /** + * The new remote hostname to which the server referred the connection. + * @returns {string} + * the hostname + */ + getHostname() { + return this.getResponse().getResponseCode().getHostname(); + } + + /** + * The new remote port to which the server referred the connection. + * @returns {string} + * the port + */ + getPort() { + return this.getResponse().getResponseCode().getPort(); + } +} + +export { + SieveClientException, + SieveReferralException, + SieveServerException, + SieveTimeOutException, + SieveCertValidationException, + SieveException +}; diff --git a/src/common/libManageSieve/SieveLogger.mjs b/src/common/libManageSieve/SieveLogger.mjs new file mode 100644 index 00000000..49d46db4 --- /dev/null +++ b/src/common/libManageSieve/SieveLogger.mjs @@ -0,0 +1,288 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +const TWO_CHARS = 2; +const THREE_CHARS = 3; + +const BASE_10 = 10; + +// eslint-disable-next-line no-magic-numbers +const LOG_REQUEST = (1 << 0); +// eslint-disable-next-line no-magic-numbers +const LOG_RESPONSE = (1 << 1); +// eslint-disable-next-line no-magic-numbers +const LOG_STATE = (1 << 2); +// eslint-disable-next-line no-magic-numbers +const LOG_STREAM = (1 << 3); +// eslint-disable-next-line no-magic-numbers +const LOG_SESSION_INFO = (1 << 4); +// eslint-disable-next-line no-magic-numbers +const LOG_TRACE = (1 << 5); + +const DEFAULT_LEVEL = 0; + +/** + * Implements a common and platform independent logging interface. + * The log level is interpreted as a bit filed with turns logging + * for the specified scope on and of. + * + * The level is concerning scopes and does not differentiate between + * warning, error and info. + */ +class SieveLogger { + + /** + * Creates a new instance + * @param {string} [prefix] + * an optional prefix for this logger. + * @param {int} [level] + * the logger level + * + */ + constructor(prefix, level) { + if (typeof (prefix) === "undefined") + prefix = ""; + + if (typeof (level) === "undefined") + level = DEFAULT_LEVEL; + + this._level = level; + this._prefix = prefix; + } + + /** + * Logs a request related information + * @param {string} message + * the request status to log. + * @returns {SieveLogger} + * a self reference + */ + logRequest(message) { + return this.log(message, LOG_REQUEST); + } + + /** + * Logs response related information + * @param {byte[]} data + * the response status to log + * @returns {SieveLogger} + * a self reference + */ + logResponse(data) { + const byteArray = new Uint8Array(data.slice(0, data.length)); + + return this.log( + "Server -> Client\n" + (new TextDecoder("UTF-8")).decode(byteArray), + LOG_RESPONSE); + } + + /** + * Logs state machine information. + * @param {string} message + * the stat information to log. + * @returns {SieveLogger} + * a self reference + */ + logState(message) { + return this.log(message, LOG_STATE); + } + + /** + * Dumps raw stream data to the log + * @param {string} message + * the stream information to log. + * @returns {SieveLogger} + * a self reference + */ + logStream(message) { + return this.log(message, LOG_STREAM); + } + + /** + * Logs information about the session. + * @param {string} message + * the message to log. + * @returns {SieveLogger} + * a self reference + */ + logSession(message) { + return this.log(message, LOG_SESSION_INFO); + } + + /** + * Logs the given message to the browser console. + * + * @param {string} message + * The message which should be logged + * @param {int} [level] + * the log level. If omitted the message will be always logged. + * @returns {SieveLogger} + * a self reference + */ + log(message, level) { + if (!this.isLoggable(level)) + return this; + + if (this.isLoggable(LOG_TRACE)) { + // eslint-disable-next-line no-console + console.trace(`[${this.getTimestamp()} ${this.prefix()}] ${message}`); + return this; + } + + // eslint-disable-next-line no-console + console.log(`[${this.getTimestamp()} ${this.prefix()}] ${message}`); + return this; + } + + /** + * Checks if state information should be logged. + * + * @returns {boolean} + * true in case state information should be logged otherwise false. + */ + isLevelState() { + return this.isLoggable(LOG_STATE); + } + + /** + * Checks if session information should be logged. + * + * @returns {boolean} + * true in case session information should be logged otherwise false. + */ + isLevelSession() { + return this.isLoggable(LOG_SESSION_INFO); + } + + /** + * Tests if the log level should log. + * + * @param {int} level + * the level which should be checked. + * @returns {boolean} + * true in case the log level is activated otherwise false + */ + isLoggable(level) { + if (typeof (level) === "undefined") + return true; + + return !!(this.level() & level); + } + + /** + * Checks if stream data should be logged. + * + * @returns {boolean} + * true in case the stream data should be logged otherwise false + */ + isLevelStream() { + return this.isLoggable(LOG_STREAM); + } + + /** + * Checks if request data should be logged. + * + * @returns {boolean} + * true in case the request data should be logged otherwise false + */ + isLevelRequest() { + return this.isLoggable(LOG_REQUEST); + } + + /** + * Checks if response data should be logged. + * + * @returns {boolean} + * true in case the response data should be logged otherwise false + */ + isLevelResponse() { + return this.isLoggable(LOG_RESPONSE); + } + + /** + * Gets and sets the log level to the given bit mask. + * Note that the log level is a bit mask, every bit in the + * bit mask corresponds to a special logger. + * + * In order to activate or deactivate a logger you need to + * get the level toggle the desired bits and set the new level. + * + * @param {int} [level] + * the desired log level as bit mask. + * @returns {int} + * the current log level + */ + level(level) { + if (typeof (level) !== "undefined") + this._level = level; + + return this._level; + } + + + /** + * Pads the given string with leading zeros + * @private + * + * @param {string} n + * the string which should be padded + * @param {int} m + * the maximum padding. + * + * @returns {string} + * the padded string + */ + _pad(n, m) { + + let str = n; + + for (let i = 0; i < m; i++) + if (n < Math.pow(BASE_10, i)) + str = '0' + str; + + return str; + } + + /** + * Gets the current time in iso format (hh:mm:ss.SSS) + * + * @returns {string} + * the current timestamp as string. + */ + getTimestamp() { + + const date = new Date(); + return this._pad(date.getHours(), TWO_CHARS) + + ":" + this._pad(date.getMinutes(), TWO_CHARS) + + ":" + this._pad(date.getSeconds(), TWO_CHARS) + + "." + this._pad(date.getMilliseconds(), THREE_CHARS); + } + + + /** + * Gets and sets the loggers prefix. The prefix is appended to + * every logger message + * + * @param {string} [prefix] + * the new prefix. + * @returns {string} + * the current prefix. + */ + prefix(prefix) { + + if (typeof (prefix) !== "undefined") + this._prefix = prefix; + + return this._prefix; + } +} + +export { SieveLogger }; diff --git a/src/common/libManageSieve/SieveRequest.js b/src/common/libManageSieve/SieveRequest.js deleted file mode 100755 index 47f45898..00000000 --- a/src/common/libManageSieve/SieveRequest.js +++ /dev/null @@ -1,1446 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -/* - The communication in this library is asynchronous! After sending a request, - you will be notified by a listener, as soon as a response arrives. - - If a request caused an error or timeout, its error listener will be called - to resolve the issue. If a server rejects a request, the onError() function - of the error listener will be invoked. In case of a timeout situation, the - onTimeout() function is called. - - If a request succeeded, the corresponding response listener of the request - will be notified. - - The addResponse(), getNextRequest(), hasNextRequest(), cancel() Methods are - used by the Sieve object, and should not be invoked manually. - - When the sieve object receives a response, it is passed to the addResponse() - Method of the requesting object. A timeout is signaled by passing invoking - the cancel() Method. - -*/ - -(function (exports) { - - // Enable Strict Mode - "use strict"; - - const { - SieveSimpleResponse, - SieveCapabilitiesResponse, - SieveListScriptsResponse, - SieveSaslLoginResponse, - SieveSaslCramMd5Response, - SieveGetScriptResponse, - SieveSaslScramShaResponse - } = require("./SieveResponse.js"); - - const { SieveCrypto } = require("./SieveCrypto.js"); - - const STATE_CRAM_MD5_INITIALIZED = 0; - const STATE_CRAM_MD5_CHALLENGED = 1; - const STATE_CRAM_MD5_COMPLETED = 4; - - const STATE_LOGIN_INITIALIZED = 0; - const STATE_LOGIN_USERNAME = 1; - const STATE_LOGIN_PASSWORD = 2; - const STATE_LOGIN_COMPLETED = 4; - - const RESPONSE_OK = 0; - const RESPONSE_BYE = 1; - const RESPONSE_NO = 2; - - const SEED = 1234567890; - - /** - * An abstract class, it is the prototype for any requests - */ - class SieveAbstractRequest { - - /** - * Creates a new Instance. - */ - constructor() { - this.errorListener = null; - this.timeoutListener = null; - this.byeListener = null; - - this.responseListener = null; - } - - /** - * The error listener is called whenever the server returns an error state - * @param {Function} listener - * the listener which should be invoked - * @returns {SieveAbstractRequest} - * a self reference - */ - addErrorListener(listener) { - - if (typeof listener !== 'function') { - throw new Error("Error listener is not a function"); - } - - this.errorListener = listener; - return this; - } - - /** - * The timeout listener is calls whenever sending a request fails for some - * reason. This could be because of a timeout or because the server terminated - * the connection or something else happened. - * - * The listener does not necessarily wait for a timeout event. E.g. in case the - * connection is lost it will fire immediately. - * - * @param {Function} listener - * the listener which should be invoked - * @returns {SieveAbstractRequest} - * a self reference - */ - addTimeoutListener(listener) { - - // TODO should be renamed to error listener as it is more than just a timeout handler... - if (typeof listener !== 'function') { - throw new Error("Timeout listener is not a function"); - } - - this.timeoutListener = listener; - return this; - } - - /** - * The bye listener is called whenever the server terminated the connection - * gracefully. - * - * @param {Function} listener - * the listener which should be invoked - * @returns {SieveAbstractRequest} - * a self reference - */ - addByeListener(listener) { - - if (typeof listener !== 'function') { - throw new Error("Bye listener is not a function"); - } - - this.byeListener = listener; - return this; - } - - /** - * Add a response listener to this request. The response listener will - * be triggered in case a successful server response was received - * - * @param {Function} listener - * the listener which should be invoked when the server response - * @returns {SieveAbstractRequest} - * a self reference - */ - addResponseListener(listener) { - - if (typeof listener !== 'function') { - throw new Error(`Listener is not a function ${listener}`); - } - - this.responseListener = listener; - return this; - } - - /** - * In general Sieve uses an unsolicited communication. - * The client sends messages to server and the server responds - * to those. - * - * But there are some exceptions to this rule, e.g. the - * init request upon connecting or after tls completed. - * Both are send by the server to the client. - * - * @returns {boolean} - * true in case the request is unsolicited. Which means - * the client sends a request and the server responds - * to that. - * false in case the request is solicited. Which means - * it was send by the server without an explicit - * request from the client. - */ - isUnsolicited() { - return true; - } - - /** - * Most request use a single request response pair. But especially the SASL - * script use normally more than one round trip due to security reasons. - * - * This flags indicates if the Request's internal state engine was completed. - * - * @returns {boolean} - * false in case the request has been completed. True in case it is still - * running and waits for sending the next request. - */ - hasNextRequest() { - return false; - } - - /** - * Returns the next request as a string. It uses the given - * Request builder to assemble the string. - * - * @param {SieveAbstractRequestBuilder} builder - * a reference to a stateless request builder which can be used - * to form the request string. - * @returns {string} - * the data which should be send to the server - * - * @abstract - */ - getNextRequest(builder) { - throw new Error(`Abstract Method implement me ${builder}`); - } - - /** - * Triggers a timeout on the error listener. - * This should never be invoked directly by any other object than the sieve connection. - * - * @param {Error} [reason] - * the optional reason why the request was canceled. - */ - cancel(reason) { - if (this.timeoutListener) - this.timeoutListener(reason); - } - - /** - * Trigger the error listener to signal an error condition. - * - * This should never be invoked directly by any other object than the - * sieve connection or this class. - * - * @param {SieveSimpleResponse} response - * the response which should be handled by this request. - */ - onNo(response) { - if (this.errorListener) - this.errorListener(response); - } - - /** - * Trigger the bye listener. - * - * This should never be invoked directly by any other object than the - * sieve connection or this class. - * - * @param {SieveSimpleResponse} response - * the response which should be handled by this request. - */ - onBye(response) { - if ((response.getResponse() === RESPONSE_BYE) && (this.byeListener)) - this.byeListener(response); - } - - /** - * Triggers the ok listener. This is normally when the request completed - * successfully. - * - * This should never be invoked directly by any other object than the - * sieve connection or this class. - * - * @param {SieveSimpleResponse} response - * the response which should be handled by this request. - * - * @abstract - */ - onOk(response) { - throw new Error(`Abstract Method override me ${response}`); - } - - /** - * An abstract helper, which calls the default message handlers - * for the given response - * - * @param {SieveSimpleResponse} response - * the response which should be handled by this request. - * @returns {SieveAbstractRequest} - * a self reference - */ - addResponse(response) { - if (response.getResponse() === RESPONSE_OK) - this.onOk(response); - else if (response.getResponse() === RESPONSE_BYE) - this.onBye(response); - else if (response.getResponse() === RESPONSE_NO) - this.onNo(response); - else - throw new Error("Invalid Response Code"); - - return this; - } - } - - /** - * An abstract calls derived from AbstractRequest. It is the foundation for - * any requests implementing a SASL compatible authentication. - */ - class SieveAbstractSaslRequest extends SieveAbstractRequest { - - /** - * @inheritdoc - */ - constructor() { - super(); - - this._username = ""; - this._password = ""; - this._authorization = ""; - } - - /** - * Sets the sasl mechanisms username. Not all SASL Mechanisms require an username. - * - * @param {string} username - * the username - * @returns {SieveAbstractSaslRequest} - * a self reference - **/ - setUsername(username) { - this._username = username; - return this; - } - - /** - * Most SASL mechanisms need a password or secret to authenticate. - * But there are also mechanisms like SASL EXTERNAL which do not need a password. - * They use different methods to transfer the credentials. - * - * @returns {boolean} - * indicates if this SASL Mechanism needs a password - */ - hasPassword() { - return true; - } - - /** - * Sets the sasl request's password. - * - * @param {string} password - * the password which shall be used for the authentication. - * @returns {SieveAbstractSaslRequest} - * a self reference - **/ - setPassword(password) { - this._password = password; - return this; - } - - /** - * Checks if this mechanism supports authorization. Keep in mind - * authorization is rarely used and only very few mechanisms - * support it. - * - * With authorization you use your credentials to login as a different user. - * Which means you first authenticate with your username and then do the - * authorization which switch the user. Typically admins and superusers have - * such super powers. - * - * @returns {boolean} - * true in case the request supports authorization otherwise false. - */ - isAuthorizable() { - // Sub classes shall overwrite this with true in case authorization is supported - return false; - } - - /** - * Sets the username which should be authorized. - * In case authorization is not supported it will be silently ignored. - * - * @param {string} authorization - * the username used for authorization - * @returns {SieveAbstractRequest} - * a self reference - **/ - setAuthorization(authorization) { - if (this.isAuthorizable()) - this._authorization = authorization; - - return this; - } - - /** - * @inheritdoc - */ - onOk(response) { - if (this.responseListener) - this.responseListener(response); - } - } - - /** - * Loads a script from the server and returns the content. - * In case the script is non existent an error will be triggered. - */ - class SieveGetScriptRequest extends SieveAbstractRequest { - - /** - * Create a new request which load a sieve script from the - * remote server. - * - * @param {string} script - * the script which should be retrieved - */ - constructor(script) { - super(); - this.script = script; - } - - /** - * @inheritdoc - */ - getNextRequest(builder) { - return builder - .addLiteral("GETSCRIPT") - .addQuotedString(this.script); - } - - /** - * @inheritdoc - */ - onOk(response) { - if (this.responseListener) - this.responseListener(response); - } - - /** - * @inheritdoc - */ - addResponse(parser) { - return super.addResponse( - (new SieveGetScriptResponse(this.script)).parse(parser)); - } - } - - /** - * Stores the given script on the server. - * The script is validated by the server and will be rejected with a NO - * in case the validation fails. - * - * Please not it will overwrite silently any existing script with the same name. - */ - class SievePutScriptRequest extends SieveAbstractRequest { - - /** - * Creates a request which stores a sieve script on the server. - * - * @param {string} script - * the script's name - * @param {string} body - * the sieve script which should be stored on the server. - */ - constructor(script, body) { - super(); - this.script = script; - - // cleanup line breaks... - - // eslint-disable-next-line no-control-regex - this.body = body.replace(/\r\n|\r|\n|\u0085|\u000C|\u2028|\u2029/g, "\r\n"); - } - - /** - * @inheritdoc - */ - getNextRequest(builder) { - return builder - .addLiteral("PUTSCRIPT") - .addQuotedString(this.script) - .addMultiLineString(this.body); - } - - /** - * @inheritdoc - */ - onOk(response) { - if (this.responseListener) - this.responseListener(response); - } - - /** - * @inheritdoc - */ - addResponse(parser) { - return super.addResponse( - (new SieveSimpleResponse()).parse(parser)); - } - } - - /** - * The CheckScriptRequest validates the Syntax of a Sieve script. The script - * is not stored on the server. - * - * If the script fails this test, the server replies with a NO response. The - * response contains one or more CRLF separated error messages. - * - * An OK response can contain Syntax Warnings. - * - * C: CheckScript {31+} - * C: #comment - * C: InvalidSieveCommand - * C: - * S: NO "line 2: Syntax error" - * - */ - class SieveCheckScriptRequest extends SieveAbstractRequest { - - /** - * Creates a new request which checks if the scripts' syntax is valid. - * @param {string} body - * the script which should be check for syntactical validity - */ - constructor(body) { - super(); - // Strings in JavaScript should use the encoding of the xul document and... - // ... sockets use binary strings. That means for us we have to convert... - // ... the JavaScript string into a UTF8 String. - - // Further more Sieve expects line breaks to be \r\n. Mozilla uses \n ... - // ... according to the documentation. But for some unknown reason a ... - // ... string sometimes contains mixed line breaks. Thus we convert ... - // ... any \r\n, \r and \n to \r\n. - - // eslint-disable-next-line no-control-regex - this.body = body.replace(/\r\n|\r|\n|\u0085|\u000C|\u2028|\u2029/g, "\r\n"); - } - - /** - * @inheritdoc - */ - getNextRequest(builder) { - return builder.addLiteral("CHECKSCRIPT") - .addMultiLineString(this.body); - } - - /** - * @inheritdoc - */ - onOk(response) { - if (this.responseListener) - this.responseListener(response); - } - - /** - * @inheritdoc - */ - addResponse(parser) { - return super.addResponse( - (new SieveSimpleResponse()).parse(parser)); - } - } - - /** - * This class encapsulates a Sieve SETACTIVE request. - * <p> - * Either none or one server scripts can be active, this means you can't have - * more than one active scripts - * <p> - * You activate a Script by calling SETACTIVE and the script name. At activation - * the previous active Script will become inactive. - */ - class SieveSetActiveRequest extends SieveAbstractRequest { - - /** - * Creates a new request which activates the given script - * @param {string} script - The script name which should be activated. Passing - * an empty string deactivates the active script. - */ - constructor(script) { - super(); - - this.script = ""; - - if ((typeof (script) !== 'undefined') && (script !== null)) - this.script = script; - } - - /** - * @inheritdoc - */ - getNextRequest(builder) { - return builder - .addLiteral("SETACTIVE") - .addQuotedString(this.script); - } - - /** - * @inheritdoc - */ - onOk(response) { - if (this.responseListener) - this.responseListener(response); - } - - /** - * @inheritdoc - */ - addResponse(parser) { - return super.addResponse( - (new SieveSimpleResponse()).parse(parser)); - } - } - - /** - * The capability request asks the server to transmit his - * capabilities like the supported sieve extensions, as - * well as a list with all possible SASL authentication mechanisms. - * - * The returned capabilities depends upon the context. A server may - * refuse to advertise SASL Mechanisms while using an insecure - * connection. As soon as you started a secure connection it may offer - * additional Mechanisms. - * - * So you should always refresh the server's capabilities. - */ - class SieveCapabilitiesRequest extends SieveAbstractRequest { - - /** - * @inheritdoc - */ - getNextRequest(builder) { - return builder - .addLiteral("CAPABILITY"); - } - - /** - * @inheritdoc - */ - onOk(response) { - if (this.responseListener) - this.responseListener(response); - } - - /** - * @inheritdoc - */ - addResponse(parser) { - return super.addResponse( - (new SieveCapabilitiesResponse()).parse(parser)); - } - } - - /** - * The delete script command is used to remove a script from the server. - * Deleting an non existing script will result in an error. Also deleting - * the active script will result in an error. - */ - class SieveDeleteScriptRequest extends SieveAbstractRequest { - - /** - * Creates a request which deletes the given script. - * @param {string} script - * the scripts name which should be deleted - */ - constructor(script) { - super(); - this.script = script; - } - - /** - * @inheritdoc - */ - getNextRequest(builder) { - return builder - .addLiteral("DELETESCRIPT") - .addQuotedString(this.script); - } - - /** - * @inheritdoc - */ - onOk(response) { - if (this.responseListener) - this.responseListener(response); - } - - /** - * @inheritdoc - */ - addResponse(parser) { - return super.addResponse( - (new SieveSimpleResponse()).parse(parser)); - } - } - - /** - * The NOOP request does nothing, it is used for protocol resynchronization or - * to reset any inactivity auto-logout timer on the server. - * - * The response to the NOOP command is always OK. - */ - class SieveNoopRequest extends SieveAbstractRequest { - - /** - * @inheritdoc - */ - getNextRequest(builder) { - return builder - .addLiteral("NOOP"); - } - - /** - * @inheritdoc - */ - onOk(response) { - if (this.responseListener) - this.responseListener(response); - } - - /** - * @inheritdoc - */ - addResponse(parser) { - return super.addResponse( - (new SieveSimpleResponse()).parse(parser)); - } - } - - - /** - * This command is used to rename a Sieve script. The Server will reply with - * a NO response if the old script does not exist, or a script with the new - * name already exists. - * - * Renaming the active script is allowed, the server ensures that the - * renamed script remains active. - */ - class SieveRenameScriptRequest extends SieveAbstractRequest { - - /** - * Creates a rename script request. - * - * @param {string} oldScript Name of the script, which should be renamed - * @param {string} newScript New name of the script - **/ - constructor(oldScript, newScript) { - super(); - this.oldScript = oldScript; - this.newScript = newScript; - } - - /** - * @inheritdoc - */ - getNextRequest(builder) { - return builder - .addLiteral("RENAMESCRIPT") - .addQuotedString(this.oldScript) - .addQuotedString(this.newScript); - } - - /** - * @inheritdoc - */ - onOk(response) { - if (this.responseListener) - this.responseListener(response); - } - - /** - * @inheritdoc - */ - addResponse(parser) { - return super.addResponse( - (new SieveSimpleResponse()).parse(parser)); - } - } - - /** - * This command is used to list all sieve script of the current user. - * In case there are no scripts the server responds with an empty list. - */ - class SieveListScriptsRequest extends SieveAbstractRequest { - - /** - * @inheritdoc - */ - getNextRequest(builder) { - return builder - .addLiteral("LISTSCRIPTS"); - } - - /** - * @inheritdoc - */ - onOk(response) { - if (this.responseListener) - this.responseListener(response); - } - - /** - * @inheritdoc - */ - addResponse(parser) { - return super.addResponse( - new SieveListScriptsResponse().parse(parser)); - } - } - - /** - * Initializes switching to tls via start tls - */ - class SieveStartTLSRequest extends SieveAbstractRequest { - - /** - * @inheritdoc - */ - getNextRequest(builder) { - return builder - .addLiteral("STARTTLS"); - } - - /** - * @inheritdoc - */ - onOk(response) { - if (this.responseListener) - this.responseListener(response); - } - - /** - * @inheritdoc - */ - addResponse(parser) { - return super.addResponse( - (new SieveSimpleResponse()).parse(parser)); - } - } - - /** - * A logout request signals the server that the client wishes to terminate - * the current session. - * <pre> - * Client > LOGOUT - * Server < OK "Logout Complete" - * [ connection terminated ] - * </pre> - * <p> - */ - class SieveLogoutRequest extends SieveAbstractRequest { - - /** - * @inheritdoc - */ - getNextRequest(builder) { - return builder.addLiteral("LOGOUT"); - } - - /** - * @inheritdoc - */ - onOk(response) { - if (this.responseListener) - this.responseListener(response); - } - - /** - * @inheritdoc - */ - onBye(response) { - // As issued a logout request thus onBye response is perfectly fine... - // ... and equivalent to an ok in this case. - this.onOk(response); - } - - /** - * @inheritdoc - */ - addResponse(parser) { - return super.addResponse( - (new SieveSimpleResponse()).parse(parser)); - } - } - - /** - * A ManageSieve server automatically post his capabilities as soon as the - * connection is established or a secure channel is successfully started - * (STARTTLS command). In order to capture this information a dummy request - * is used. It does not send a real request, but it parses the initial response - * of the sieve server. Therefore it is important to add the request before the - * connection is established. Otherwise the message queue will be jammed. - * - * Server < "IMPLEMENTATION" "Cyrus timsieved v2.1.18-IPv6-Debian-2.1.18-1+sarge2" - * < "SASL" "PLAIN" - * < "SIEVE" "fileinto reject envelope vacation imapflags notify subaddress relational regex" - * < "STARTTLS" - * < OK - * - */ - class SieveInitRequest extends SieveAbstractRequest { - - /** - * @inheritdoc - */ - onOk(response) { - if (this.responseListener) - this.responseListener(response); - } - - /** - * @inheritdoc - */ - isUnsolicited() { - return false; - } - - /** - * @inheritdoc - */ - addResponse(parser) { - return super.addResponse( - (new SieveCapabilitiesResponse()).parse(parser)); - } - } - - /** - * Implements the SASL Plain authentication method. - * - * The password is only base64 encoded not encrypted. Therefore it can be - * read or sniffed easily. A secure connection will solve this issue. Always - * start a tls session before using this request. - * - * Client > AUTHENTICATE "PLAIN" AHRlc3QAc2VjcmV0 | AUTHENTICATE "PLAIN" [UTF8NULL]test[UTF8NULL]secret - * Server < OK | OK - */ - class SieveSaslPlainRequest extends SieveAbstractSaslRequest { - - /** - * The sasl plain request always support proxy authentication. - * - * @returns {boolean} - * always true - */ - isAuthorizable() { - return true; - } - - /** - * @inheritdoc - */ - getNextRequest(builder) { - return builder - .addLiteral("AUTHENTICATE") - .addQuotedString("PLAIN") - .addQuotedBase64(`${this._authorization}\0${this._username}\0${this._password}`); - } - - /** - * @inheritdoc - */ - addResponse(parser) { - return super.addResponse( - (new SieveSimpleResponse()).parse(parser)); - } - } - - /** - * This request implements the SALS Login authentication method. It is deprecated - * and has been superseded by SASL Plain method. SASL Login uses a question and - * answer style communication. The server will request first the username and - * then the password. - * <p> - * Please note, that the password is not encrypted, it is only base64 encoded. - * Therefore it can be read or sniffed easily. A secure connection will solve - * this issue. So send whenever possible, a SieveStartTLSRequest before calling - * this request. - * <p> - * Client > AUTHENTICATE "LOGIN" | AUTHENTICATE "LOGIN" - * Server < {12} | {12} - * < VXNlcm5hbWU6 | Username: - * Client > {8+} | {8+} - * > Z2Vlaw== | geek - * Server < {12} | {12} - * < UGFzc3dvcmQ6 | Password: - * Client > {12+} | {12+} - * > dGgzZzMzazE= | th3g33k1 - * Server < OK | OK - * - * @deprecated - */ - class SieveSaslLoginRequest extends SieveAbstractSaslRequest { - - /** - * @inheritdoc - */ - constructor() { - super(); - this.response = new SieveSaslLoginResponse(); - } - - /** - * @inheritdoc - */ - getNextRequest(builder) { - switch (this.response.getState()) { - case STATE_LOGIN_INITIALIZED: - return builder - .addLiteral("AUTHENTICATE") - .addQuotedString("LOGIN"); - case STATE_LOGIN_USERNAME: - return builder - .addQuotedBase64(this._username); - case STATE_LOGIN_PASSWORD: - return builder - .addQuotedBase64(this._password); - } - - throw new Error("Unknown state in SASL login"); - } - - /** - * @inheritdoc - */ - hasNextRequest() { - if (this.response.getState() === STATE_LOGIN_COMPLETED) - return false; - - return true; - } - - /** - * @inheritdoc - */ - addResponse(parser) { - this.response.parse(parser); - - if (this.hasNextRequest()) - return this; - - return super.addResponse(this.response); - } - } - - /** - * Implements the CRAM-MD5 Challenge-Response Mechanism as defined in RFC2195. - * - * It is considered to be weak and should only be used via encrypted connections. - * - * @author Thomas Schmid - * @author Max Dittrich - */ - class SieveSaslCramMd5Request extends SieveAbstractSaslRequest { - - /** - * @inheritdoc - */ - constructor() { - super(); - this.response = new SieveSaslCramMd5Response(); - } - - /** - * @inheritdoc - */ - getCrypto() { - return new SieveCrypto("MD5"); - } - - /** - * Called when the server challenges the client to calculate a secret. - * - * @param {SieveAbstractRequestBuilder} builder - * the builder which should be used to build the response. - * @returns {SieveAbstractRequestBuilder} - * the builder which contains the response for the challenge. - */ - onChallenged(builder) { - - let challenge = this.response.getChallenge(); - // decoding the base64-encoded challenge - challenge = builder.convertFromBase64(challenge); - - const crypto = this.getCrypto(); - const hmac = crypto.HMAC(this._password, challenge, "hex"); - - return builder - .addQuotedBase64(`${this._username} ${hmac}`); - } - - /** - * @inheritdoc - */ - getNextRequest(builder) { - switch (this.response.getState()) { - case STATE_CRAM_MD5_INITIALIZED: - return builder - .addLiteral("AUTHENTICATE") - .addQuotedString("CRAM-MD5"); - case STATE_CRAM_MD5_CHALLENGED: - return this.onChallenged(builder); - } - - throw new Error("Illegal state in SASL CRAM"); - } - - /** - * @inheritdoc - */ - hasNextRequest() { - if (this.response.getState() === STATE_CRAM_MD5_COMPLETED) - return false; - - return true; - } - - /** - * @inheritdoc - */ - addResponse(parser) { - this.response.parse(parser); - - if (this.hasNextRequest()) - return this; - - return super.addResponse(this.response); - } - } - - /** - * This request implements an abstract base class for the "Salted Challenge Response Authentication - * Mechanism" (SCRAM). A SASL SCRAM-SHA-1 compatible implementation is mandatory - * for every manage sieve server. SASL SCRAM-SHA-1 supersedes DIGEST-MD5. - * - * @author Thomas Schmid - */ - class SieveAbstractSaslScramRequest extends SieveAbstractSaslRequest { - - /** - * @inheritdoc - */ - constructor() { - super(); - this.response = new SieveSaslScramShaResponse(); - } - - /** - * Checks if the request supports authorization - * - * @returns {boolean} - * true, as a scram request can always be used for a proxy authentication - */ - isAuthorizable() { - // overwrite the default as this mechanism support authorization - return true; - } - - /** - * Gets the SASL Mechanism name. - * @abstract - * - * @returns {string} - * the SASL Mechanism's unique it as string. - */ - getSaslName() { - throw new Error("Implement SASL Name"); - } - - /** - * Returns the crypto engine/provider which should be used for this request. - * @abstract - * - * @returns {SieveCrypto} - * the crypto engine - */ - getCrypto() { - throw new Error("Implement Crypto Method which returns a crypto provider"); - } - - /** - * - * @param {SieveAbstractRequestBuilder} builder - * a reference to an request builder. - * - * @returns {SieveAbstractRequestBuilder} - * a builder which contains the request data. - */ - onChallengeServer(builder) { - - const crypto = this.getCrypto(); - - this._cnonce = crypto.H("" + (Math.random() * SEED), "hex"); - - // For integration tests, we need to fake the nonce... - // ... so we take the nonce from the rfc otherwise the verification fails. - // - // ### DEBUG SHA1 ### - // this._cnonce = "fyko+d2lbbFgONRv9qkxdawL"; - // this._username = "user"; - // this._password = "pencil"; - // - // ### DEBUG SHA256 ### - // this._cnonce = "rOprNGfwEbeRWgbNEkqO"; - // this._username = "user"; - // this._password = "pencil"; - - // TODO SCRAM: escape/normalize authorization and username - // ;; UTF8-char except NUL, "=", and "," - // "=" is escaped by =2C and "," by =3D - - // Store client-first-message-bare - this._authMessage = "n=" + this._username + ",r=" + this._cnonce; - this._g2Header = "n," + (this._authorization !== "" ? "a=" + this._authorization : "") + ","; - - return builder - .addLiteral("AUTHENTICATE") - .addQuotedString(this.getSaslName()) - .addQuotedBase64(`${this._g2Header}${this._authMessage}`); - } - - /** - * - * @param {SieveAbstractRequestBuilder} builder - * a reference to an request builder. - * - * @returns {SieveAbstractRequestBuilder} - * a builder which contains the request data. - */ - onValidateChallenge(builder) { - // Check if the server returned our nonce. This should prevent... - // ... man in the middle attacks. - const nonce = this.response.getNonce(); - if ((nonce.substr(0, this._cnonce.length) !== this._cnonce)) - throw new Error("Nonce invalid"); - - const crypto = this.getCrypto(); - - // As first step we need to salt the password... - const salt = this.response.getSalt(); - const iter = this.response.getIterationCounter(); - - // TODO Normalize password; and convert it into a byte array... - // ... It might contain special characters. - - // ... this is done by applying a simplified PBKDF2 algorithm... - // ... so we endup by calling Hi(Normalize(password), salt, i) - this._saltedPassword = crypto.Hi(this._password, salt, iter); - - // the clientKey is defined as HMAC(SaltedPassword, "Client Key") - const clientKey = crypto.HMAC(this._saltedPassword, "Client Key"); - - // create the client-final-message-without-proof, ... - const msg = "c=" + builder.convertToBase64(this._g2Header) + ",r=" + nonce; - // ... append it and the server-first-message to client-first-message-bare... - this._authMessage += "," + this.response.getServerFirstMessage() + "," + msg; - - // ... and convert it into a byte array. - this._authMessage = crypto.strToByteArray(this._authMessage); - - // As next Step sign out message, this is done by applying the client... - // ... key through a pseudorandom function to the message. It is defined... - // as HMAC(H(ClientKey), AuthMessage) - const clientSignature = crypto.HMAC( - crypto.H(clientKey), - this._authMessage); - - // We now complete the cryptographic part an apply our clientkey to the... - // ... Signature, so that the server can be sure it is talking to us. - // The RFC defines this step as ClientKey XOR ClientSignature - const clientProof = clientKey; - for (let k = 0; k < clientProof.length; k++) - clientProof[k] ^= clientSignature[k]; - - // Every thing done so let's send the message... - // "c=" base64( (("" / "y") "," [ "a=" saslname ] "," ) "," "r=" c-nonce s-nonce ["," extensions] "," "p=" base64 - return builder - .addQuotedBase64(msg + ",p=" + builder.convertToBase64(clientProof)); - // return "\""+btoa(msg+",p="+btoa(this.byteArrayToStr(clientProof)))+"\"\r\n"; - } - - /** - * @inheritdoc - */ - getNextRequest(builder) { - - // Step1: Client sends Message to server. See SASL Login how to integrate it - // into the AUTHENTICATE Command. - // - // e.g.: "AUTHENTICATE \"SCRAM-SHA-1\" \"n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL\"\r\n" - - switch (this.response.getState()) { - case 0: - return this.onChallengeServer(builder); - case 1: - return this.onValidateChallenge(builder); - case 2: - // obviously we have to send an empty response. The server did not wrap... - // ... the verifier into the Response Code... - return builder - .addQuotedString(""); - // return "\"\"\r\n"; - } - - throw new Error("Illegal state in SASL SCRAM"); - } - - /** - * @inheritdoc - */ - hasNextRequest() { - if (this.response.getState() === 4) - return false; - - return true; - } - - /** - * @inheritdoc - */ - onOk(response) { - - const crypto = this.getCrypto(); - - const serverSignature = crypto.HMAC( - crypto.HMAC( - this._saltedPassword, - "Server Key"), - this._authMessage - ); - - if (response.getVerifier() !== crypto.byteArrayToStr(serverSignature)) { - response.message = "Server Signature not invalid "; - response.response = 2; - this.onNo(response); - return; - } - - super.onOk(response); - } - - /** - * @inheritdoc - */ - addResponse(parser) { - - this.response.parse(parser); - - if (this.hasNextRequest()) - return this; - - return super.addResponse(this.response); - } - } - - /** - * Implements the SCRAM-SHA-1 mechanism. - */ - class SieveSaslScramSha1Request extends SieveAbstractSaslScramRequest { - - /** - * @inheritdoc - */ - getSaslName() { - return "SCRAM-SHA-1"; - } - - /** - * @inheritdoc - */ - getCrypto() { - return new SieveCrypto("SHA1"); - } - } - - /** - * Implements the SCRAM-SHA-256 mechanism. - */ - class SieveSaslScramSha256Request extends SieveAbstractSaslScramRequest { - - /** - * @inheritdoc - */ - getSaslName() { - return "SCRAM-SHA-256"; - } - - /** - * @inheritdoc - */ - getCrypto() { - return new SieveCrypto("SHA256"); - } - } - - /** - * This request implements SASL External Mechanism (rfc4422 Appendix A). - * It's a dumb-dumb implementation, and relies upon an established tls connection. - * It tells the server to use the cert provided during the TLS handshake. - * - * @author Thomas Schmid - */ - class SieveSaslExternalRequest extends SieveAbstractSaslRequest { - - /** - * @inheritdoc - */ - isAuthorizable() { - // overwrite the default behaviour. - return true; - } - - /** - * @inheritdoc - */ - getNextRequest(builder) { - return builder - .addLiteral("AUTHENTICATE") - .addQuotedString("EXTERNAL") - .addQuotedBase64("" + this._authorization); - } - - /** - * SASL External uses the TLS Cert for authentication. - * Thus it does not rely upon any password, so this method returns always false. - * - * @returns {boolean} - * returns always false - */ - hasPassword() { - return false; - } - - /** - * @inheritdoc - */ - addResponse(parser) { - return super.addResponse.call(this, - (new SieveSimpleResponse()).parse(parser)); - } - } - - exports.SieveGetScriptRequest = SieveGetScriptRequest; - exports.SievePutScriptRequest = SievePutScriptRequest; - exports.SieveCheckScriptRequest = SieveCheckScriptRequest; - exports.SieveSetActiveRequest = SieveSetActiveRequest; - exports.SieveCapabilitiesRequest = SieveCapabilitiesRequest; - exports.SieveDeleteScriptRequest = SieveDeleteScriptRequest; - exports.SieveNoopRequest = SieveNoopRequest; - exports.SieveRenameScriptRequest = SieveRenameScriptRequest; - exports.SieveListScriptsRequest = SieveListScriptsRequest; - exports.SieveStartTLSRequest = SieveStartTLSRequest; - exports.SieveLogoutRequest = SieveLogoutRequest; - exports.SieveInitRequest = SieveInitRequest; - exports.SieveSaslPlainRequest = SieveSaslPlainRequest; - exports.SieveSaslLoginRequest = SieveSaslLoginRequest; - exports.SieveSaslCramMd5Request = SieveSaslCramMd5Request; - exports.SieveSaslScramSha1Request = SieveSaslScramSha1Request; - exports.SieveSaslScramSha256Request = SieveSaslScramSha256Request; - exports.SieveSaslExternalRequest = SieveSaslExternalRequest; - -})(module.exports || this); diff --git a/src/common/libManageSieve/SieveRequest.mjs b/src/common/libManageSieve/SieveRequest.mjs new file mode 100644 index 00000000..1813734c --- /dev/null +++ b/src/common/libManageSieve/SieveRequest.mjs @@ -0,0 +1,1482 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +/* + The communication in this library is asynchronous! After sending a request, + you will be notified by a listener, as soon as a response arrives. + + If a request caused an error or timeout, its error listener will be called + to resolve the issue. If a server rejects a request, the onError() function + of the error listener will be invoked. In case of a timeout situation, the + onTimeout() function is called. + + If a request succeeded, the corresponding response listener of the request + will be notified. + + The onResponse(), getNextRequest(), hasNextRequest() Methods are + used by the Sieve object, and should not be invoked manually. + + When the sieve object receives a response, it is passed to the onResponse() + Method of the requesting object. A timeout is signaled by passing invoking + the abort() Method. + +*/ + +import { + SieveSimpleResponse, + SieveCapabilitiesResponse, + SieveListScriptsResponse, + SieveSaslLoginResponse, + SieveGetScriptResponse, + SieveSaslScramShaResponse +} from "./SieveResponse.mjs"; + +import { SieveCrypto } from "./SieveCrypto.mjs"; + +import { SieveBase64Encoder } from "./SieveBase64.mjs"; + +const STATE_LOGIN_INITIALIZED = 0; +const STATE_LOGIN_USERNAME = 1; +const STATE_LOGIN_PASSWORD = 2; +const STATE_LOGIN_COMPLETED = 4; + +const RESPONSE_OK = 0; +const RESPONSE_BYE = 1; +const RESPONSE_NO = 2; + +const SEED = 1234567890; + +/** + * An abstract class, it is the prototype for any requests + */ +class SieveAbstractRequest { + + /** + * Creates a new Instance. + */ + constructor() { + this.errorListener = null; + this.timeoutListener = null; + this.byeListener = null; + + this.responseListener = null; + + this.optional = false; + this.abandoned = false; + } + + /** + * The error listener is called whenever the server returns an error state + * @param {Function} listener + * the listener which should be invoked + * @returns {SieveAbstractRequest} + * a self reference + */ + addErrorListener(listener) { + + if (typeof listener !== 'function') { + throw new Error("Error listener is not a function"); + } + + this.errorListener = listener; + return this; + } + + /** + * The timeout listener is calls whenever sending a request fails for some + * reason. This could be because of a timeout or because the server terminated + * the connection or something else happened. + * + * The listener does not necessarily wait for a timeout event. E.g. in case the + * connection is lost it will fire immediately. + * + * @param {Function} listener + * the listener which should be invoked + * @returns {SieveAbstractRequest} + * a self reference + */ + addTimeoutListener(listener) { + + // TODO should be renamed to error listener as it is more than just a timeout handler... + if (typeof listener !== 'function') { + throw new Error("Timeout listener is not a function"); + } + + this.timeoutListener = listener; + return this; + } + + /** + * The bye listener is called whenever the server terminated the connection + * gracefully. + * + * @param {Function} listener + * the listener which should be invoked + * @returns {SieveAbstractRequest} + * a self reference + */ + addByeListener(listener) { + + if (typeof listener !== 'function') { + throw new Error("Bye listener is not a function"); + } + + this.byeListener = listener; + return this; + } + + /** + * Add a response listener to this request. The response listener will + * be triggered in case a successful server response was received + * + * @param {Function} listener + * the listener which should be invoked when the server response + * @returns {SieveAbstractRequest} + * a self reference + */ + addResponseListener(listener) { + + if (typeof listener !== 'function') { + throw new Error(`Listener is not a function ${listener}`); + } + + this.responseListener = listener; + return this; + } + + /** + * Marks this request a optional. See isOptional for more details. + * + * @returns {SieveAbstractRequest} + * a self reference. + */ + makeOptional() { + this.optional = true; + return this; + } + + /** + * Optional means the request will be send to the server. But a server + * response is not required. This is used to workaround a common bug in + * old sieve implementations. + * + * Please note an optional request will resolve earliest after the next + * subsequent request was resolved. + * + * @returns {boolean} + * false in case this request is not optional which is the default. + */ + isOptional() { + return this.optional; + } + + /** + * In general sieve is bidirectional, client first communication. This means + * the client sends a request to server and the server responds to it. Request + * and responses typically form pairs. + * + * But there are exceptions to this rule, e.g. the init response upon + * connect or after tls handshake. In this case it is a one-way communication. + * The server sends a response without being triggered by a request. + * + * @returns {boolean} + * true in case the request is bidirectional. Which means + * the client sends a request and the server responds + * to that. + * false in case the request is unidirectional. Which means + * it was send by the server without being explicitly + * request by the client. + */ + hasRequest() { + return true; + } + + /** + * Most request use a single request response pair. But especially the SASL + * script use normally more than one round trip due to security reasons. + * + * This flags indicates if the Request's internal state engine was completed. + * + * @returns {boolean} + * false in case the request has been completed. True in case it is still + * running and waits for sending the next request. + */ + hasNextRequest() { + return false; + } + + /** + * Returns the next request as a string. It uses the given + * Request builder to assemble the string. + * + * @param {SieveRequestBuilder} builder + * a reference to a stateless request builder which can be used + * to form the request string. + * @returns {string} + * the data which should be send to the server + * + * @abstract + */ + async getNextRequest(builder) { + throw new Error(`Abstract Method implement me ${builder}`); + } + + /** + * Marks this request as abandoned because the request timedout or the socket + * was closed.This should never be invoked directly by any other object than + * the sieve connection. + * + * @param {Error} [reason] + * the optional reason why the request was abandoned. + */ + abandon(reason) { + this.abandoned = true; + this.reason = reason; + } + + /** + * Checks if this message was abandoned. + * + * @returns {boolean} + * true in case the request is abandoned otherwise false. + */ + isAbandoned() { + return this.aborted; + } + + /** + * Triggers the request timeout listener. + * + * The request has to be abandoned and non optional otherwise the + * call is silently discarded. + */ + async onAbandon() { + + if (!this.isAbandoned()) + return; + + if (this.isOptional()) + return; + + if (this.timeoutListener) + this.timeoutListener(this.reason); + } + + /** + * Trigger the error listener to signal an error condition. + * + * This should never be invoked directly by any other object than the + * sieve connection or this class. + * + * @param {SieveSimpleResponse} response + * the response which should be handled by this request. + */ + async onNo(response) { + if (this.errorListener) + this.errorListener(response); + } + + /** + * Trigger the bye listener. + * + * This should never be invoked directly by any other object than the + * sieve connection or this class. + * + * @param {SieveSimpleResponse} response + * the response which should be handled by this request. + */ + async onBye(response) { + if ((response.getResponse() === RESPONSE_BYE) && (this.byeListener)) + this.byeListener(response); + } + + /** + * Triggers the ok listener. This is normally when the request completed + * successfully. + * + * This should never be invoked directly by any other object than the + * sieve connection or this class. + * + * @param {SieveSimpleResponse} response + * the response which should be handled by this request. + * + * @abstract + */ + async onOk(response) { + throw new Error(`Abstract method override me ${response}`); + } + + /** + * An abstract helper, which calls the default message handlers + * for the given response + * + * @param {SieveSimpleResponse} response + * the response which should be handled by this request. + * @returns {SieveAbstractRequest} + * a self reference + */ + async onResponse(response) { + + if (response.getResponse() === RESPONSE_OK) { + await this.onOk(response); + return this; + } + + if (this.isOptional()) + throw new Error("Invalid Response for an optional Request"); + + if (response.getResponse() === RESPONSE_BYE) { + await this.onBye(response); + return this; + } + + if (response.getResponse() === RESPONSE_NO) { + await this.onNo(response); + return this; + } + + throw new Error("Invalid Response Code"); + } +} + +/** + * An abstract calls derived from AbstractRequest. It is the foundation for + * any requests implementing a SASL compatible authentication. + */ +class SieveAbstractSaslRequest extends SieveAbstractRequest { + + /** + * @inheritdoc + */ + constructor() { + super(); + + this._username = ""; + this._password = ""; + this._authorization = ""; + } + + /** + * Sets the sasl mechanisms username. Not all SASL Mechanisms require an username. + * + * @param {string} username + * the username + * @returns {SieveAbstractSaslRequest} + * a self reference + **/ + setUsername(username) { + this._username = username; + return this; + } + + /** + * Most SASL mechanisms need a password or secret to authenticate. + * But there are also mechanisms like SASL EXTERNAL which do not need a password. + * They use different methods to transfer the credentials. + * + * @returns {boolean} + * indicates if this SASL Mechanism needs a password + */ + hasPassword() { + return true; + } + + /** + * Sets the sasl request's password. + * + * @param {string} password + * the password which shall be used for the authentication. + * @returns {SieveAbstractSaslRequest} + * a self reference + **/ + setPassword(password) { + this._password = password; + return this; + } + + /** + * Checks if this mechanism supports authorization. Keep in mind + * authorization is rarely used and only very few mechanisms + * support it. + * + * With authorization you use your credentials to login as a different user. + * Which means you first authenticate with your username and then do the + * authorization which switch the user. Typically admins and superusers have + * such super powers. + * + * @returns {boolean} + * true in case the request supports authorization otherwise false. + */ + isAuthorizable() { + // Sub classes shall overwrite this with true in case authorization is supported + return false; + } + + /** + * Sets the username which should be authorized. + * In case authorization is not supported it will be silently ignored. + * + * @param {string} authorization + * the username used for authorization + * @returns {SieveAbstractRequest} + * a self reference + **/ + setAuthorization(authorization) { + if (this.isAuthorizable()) + this._authorization = authorization; + + return this; + } + + /** + * @inheritdoc + */ + async onOk(response) { + if (this.responseListener) + this.responseListener(response); + } +} + +/** + * Loads a script from the server and returns the content. + * In case the script is non existent an error will be triggered. + */ +class SieveGetScriptRequest extends SieveAbstractRequest { + + /** + * Create a new request which load a sieve script from the + * remote server. + * + * @param {string} script + * the script which should be retrieved + */ + constructor(script) { + super(); + this.script = script; + } + + /** + * @inheritdoc + */ + async getNextRequest(builder) { + return builder + .addLiteral("GETSCRIPT") + .addQuotedString(this.script); + } + + /** + * @inheritdoc + */ + async onOk(response) { + if (this.responseListener) + this.responseListener(response); + } + + /** + * @inheritdoc + */ + async onResponse(parser) { + return await (super.onResponse( + await (new SieveGetScriptResponse(this.script)).parse(parser))); + } +} + +/** + * Stores the given script on the server. + * The script is validated by the server and will be rejected with a NO + * in case the validation fails. + * + * Please not it will overwrite silently any existing script with the same name. + */ +class SievePutScriptRequest extends SieveAbstractRequest { + + /** + * Creates a request which stores a sieve script on the server. + * + * @param {string} script + * the script's name + * @param {string} body + * the sieve script which should be stored on the server. + */ + constructor(script, body) { + super(); + this.script = script; + + // cleanup line breaks... + + // eslint-disable-next-line no-control-regex + this.body = body.replace(/\r\n|\r|\n|\u0085|\u000C|\u2028|\u2029/g, "\r\n"); + } + + /** + * @inheritdoc + */ + async getNextRequest(builder) { + return builder + .addLiteral("PUTSCRIPT") + .addQuotedString(this.script) + .addMultiLineString(this.body); + } + + /** + * @inheritdoc + */ + async onOk(response) { + if (this.responseListener) + this.responseListener(response); + } + + /** + * @inheritdoc + */ + async onResponse(parser) { + return await(super.onResponse( + await (new SieveSimpleResponse()).parse(parser))); + } +} + +/** + * The CheckScriptRequest validates the Syntax of a Sieve script. The script + * is not stored on the server. + * + * If the script fails this test, the server replies with a NO response. The + * response contains one or more CRLF separated error messages. + * + * An OK response can contain Syntax Warnings. + * + * C: CheckScript {31+} + * C: #comment + * C: InvalidSieveCommand + * C: + * S: NO "line 2: Syntax error" + * + */ +class SieveCheckScriptRequest extends SieveAbstractRequest { + + /** + * Creates a new request which checks if the scripts' syntax is valid. + * @param {string} body + * the script which should be check for syntactical validity + */ + constructor(body) { + super(); + // Strings in JavaScript should use the encoding of the xul document and... + // ... sockets use binary strings. That means for us we have to convert... + // ... the JavaScript string into a UTF8 String. + + // Further more Sieve expects line breaks to be \r\n. Mozilla uses \n ... + // ... according to the documentation. But for some unknown reason a ... + // ... string sometimes contains mixed line breaks. Thus we convert ... + // ... any \r\n, \r and \n to \r\n. + + // eslint-disable-next-line no-control-regex + this.body = body.replace(/\r\n|\r|\n|\u0085|\u000C|\u2028|\u2029/g, "\r\n"); + } + + /** + * @inheritdoc + */ + async getNextRequest(builder) { + return builder.addLiteral("CHECKSCRIPT") + .addMultiLineString(this.body); + } + + /** + * @inheritdoc + */ + async onOk(response) { + if (this.responseListener) + this.responseListener(response); + } + + /** + * @inheritdoc + */ + async onResponse(parser) { + return await(super.onResponse( + await (new SieveSimpleResponse()).parse(parser))); + } +} + +/** + * This class encapsulates a Sieve SETACTIVE request. + * <p> + * Either none or one server scripts can be active, this means you can't have + * more than one active scripts + * <p> + * You activate a Script by calling SETACTIVE and the script name. At activation + * the previous active Script will become inactive. + */ +class SieveSetActiveRequest extends SieveAbstractRequest { + + /** + * Creates a new request which activates the given script + * @param {string} script - The script name which should be activated. Passing + * an empty string deactivates the active script. + */ + constructor(script) { + super(); + + this.script = ""; + + if ((typeof (script) !== 'undefined') && (script !== null)) + this.script = script; + } + + /** + * @inheritdoc + */ + async getNextRequest(builder) { + return builder + .addLiteral("SETACTIVE") + .addQuotedString(this.script); + } + + /** + * @inheritdoc + */ + async onOk(response) { + if (this.responseListener) + this.responseListener(response); + } + + /** + * @inheritdoc + */ + async onResponse(parser) { + return await(super.onResponse( + await (new SieveSimpleResponse()).parse(parser))); + } +} + +/** + * The capability request asks the server to transmit his + * capabilities like the supported sieve extensions, as + * well as a list with all possible SASL authentication mechanisms. + * + * The returned capabilities depends upon the context. A server may + * refuse to advertise SASL Mechanisms while using an insecure + * connection. As soon as you started a secure connection it may offer + * additional Mechanisms. + * + * So you should always refresh the server's capabilities. + */ +class SieveCapabilitiesRequest extends SieveAbstractRequest { + + /** + * @inheritdoc + */ + async getNextRequest(builder) { + return builder + .addLiteral("CAPABILITY"); + } + + /** + * @inheritdoc + */ + async onOk(response) { + if (this.responseListener) + this.responseListener(response); + } + + /** + * @inheritdoc + */ + async onResponse(parser) { + return await(super.onResponse( + await (new SieveCapabilitiesResponse()).parse(parser))); + } +} + +/** + * The delete script command is used to remove a script from the server. + * Deleting an non existing script will result in an error. Also deleting + * the active script will result in an error. + */ +class SieveDeleteScriptRequest extends SieveAbstractRequest { + + /** + * Creates a request which deletes the given script. + * @param {string} script + * the scripts name which should be deleted + */ + constructor(script) { + super(); + this.script = script; + } + + /** + * @inheritdoc + */ + async getNextRequest(builder) { + return builder + .addLiteral("DELETESCRIPT") + .addQuotedString(this.script); + } + + /** + * @inheritdoc + */ + async onOk(response) { + if (this.responseListener) + this.responseListener(response); + } + + /** + * @inheritdoc + */ + async onResponse(parser) { + return await(super.onResponse( + await (new SieveSimpleResponse()).parse(parser))); + } +} + +/** + * The NOOP request does nothing, it is used for protocol resynchronization or + * to reset any inactivity auto-logout timer on the server. + * + * The response to the NOOP command is always OK. + */ +class SieveNoopRequest extends SieveAbstractRequest { + + /** + * @inheritdoc + */ + async getNextRequest(builder) { + return builder + .addLiteral("NOOP"); + } + + /** + * @inheritdoc + */ + async onOk(response) { + if (this.responseListener) + this.responseListener(response); + } + + /** + * @inheritdoc + */ + async onResponse(parser) { + return await(super.onResponse( + await (new SieveSimpleResponse()).parse(parser))); + } +} + + +/** + * This command is used to rename a Sieve script. The Server will reply with + * a NO response if the old script does not exist, or a script with the new + * name already exists. + * + * Renaming the active script is allowed, the server ensures that the + * renamed script remains active. + */ +class SieveRenameScriptRequest extends SieveAbstractRequest { + + /** + * Creates a rename script request. + * + * @param {string} oldScript Name of the script, which should be renamed + * @param {string} newScript New name of the script + **/ + constructor(oldScript, newScript) { + super(); + this.oldScript = oldScript; + this.newScript = newScript; + } + + /** + * @inheritdoc + */ + async getNextRequest(builder) { + return builder + .addLiteral("RENAMESCRIPT") + .addQuotedString(this.oldScript) + .addQuotedString(this.newScript); + } + + /** + * @inheritdoc + */ + async onOk(response) { + if (this.responseListener) + this.responseListener(response); + } + + /** + * @inheritdoc + */ + async onResponse(parser) { + return await(super.onResponse( + await (new SieveSimpleResponse()).parse(parser))); + } +} + +/** + * This command is used to list all sieve script of the current user. + * In case there are no scripts the server responds with an empty list. + */ +class SieveListScriptsRequest extends SieveAbstractRequest { + + /** + * @inheritdoc + */ + async getNextRequest(builder) { + return builder + .addLiteral("LISTSCRIPTS"); + } + + /** + * @inheritdoc + */ + async onOk(response) { + if (this.responseListener) + this.responseListener(response); + } + + /** + * @inheritdoc + */ + async onResponse(parser) { + return await(super.onResponse( + await (new SieveListScriptsResponse()).parse(parser))); + } +} + +/** + * Initializes switching to tls via start tls + */ +class SieveStartTLSRequest extends SieveAbstractRequest { + + /** + * @inheritdoc + */ + async getNextRequest(builder) { + return builder + .addLiteral("STARTTLS"); + } + + /** + * @inheritdoc + */ + async onOk(response) { + if (this.responseListener) + this.responseListener(response); + } + + /** + * @inheritdoc + */ + async onResponse(parser) { + return await(super.onResponse( + await (new SieveSimpleResponse()).parse(parser))); + } +} + +/** + * A logout request signals the server that the client wishes to terminate + * the current session. + * <pre> + * Client > LOGOUT + * Server < OK "Logout Complete" + * [ connection terminated ] + * </pre> + * <p> + */ +class SieveLogoutRequest extends SieveAbstractRequest { + + /** + * @inheritdoc + */ + async getNextRequest(builder) { + return builder.addLiteral("LOGOUT"); + } + + /** + * @inheritdoc + */ + async onOk(response) { + if (this.responseListener) + this.responseListener(response); + } + + /** + * @inheritdoc + */ + async onBye(response) { + // As issued a logout request thus onBye response is perfectly fine... + // ... and equivalent to an ok in this case. + await this.onOk(response); + } + + /** + * @inheritdoc + */ + async onResponse(parser) { + return await(super.onResponse( + await (new SieveSimpleResponse()).parse(parser))); + } +} + +/** + * A ManageSieve server automatically post his capabilities as soon as the + * connection is established or a secure channel is successfully started + * (STARTTLS command). In order to capture this information a dummy request + * is used. It does not send a real request, but it parses the initial response + * of the sieve server. Therefore it is important to add the request before the + * connection is established. Otherwise the message queue will be jammed. + * + * Server < "IMPLEMENTATION" "Cyrus timsieved v2.1.18-IPv6-Debian-2.1.18-1+sarge2" + * < "SASL" "PLAIN" + * < "SIEVE" "fileinto reject envelope vacation imapflags notify subaddress relational regex" + * < "STARTTLS" + * < OK + * + */ +class SieveInitRequest extends SieveAbstractRequest { + + /** + * @inheritdoc + */ + async onOk(response) { + if (this.responseListener) + this.responseListener(response); + } + + /** + * @inheritdoc + */ + hasRequest() { + return false; + } + + /** + * @inheritdoc + */ + async onResponse(parser) { + return await(super.onResponse( + await (new SieveCapabilitiesResponse()).parse(parser))); + } +} + +/** + * Implements the SASL Plain authentication method. + * + * The password is only base64 encoded not encrypted. Therefore it can be + * read or sniffed easily. A secure connection will solve this issue. Always + * start a tls session before using this request. + * + * Client > AUTHENTICATE "PLAIN" AHRlc3QAc2VjcmV0 | AUTHENTICATE "PLAIN" [UTF8NULL]test[UTF8NULL]secret + * Server < OK | OK + */ +class SieveSaslPlainRequest extends SieveAbstractSaslRequest { + + /** + * The sasl plain request always support proxy authentication. + * + * @returns {boolean} + * always true + */ + isAuthorizable() { + return true; + } + + /** + * @inheritdoc + */ + async getNextRequest(builder) { + return builder + .addLiteral("AUTHENTICATE") + .addQuotedString("PLAIN") + .addQuotedBase64(`${this._authorization}\0${this._username}\0${this._password}`); + } + + /** + * @inheritdoc + */ + async onResponse(parser) { + return await(super.onResponse( + await (new SieveSimpleResponse()).parse(parser))); + } +} + +/** + * This request implements the SASL Login authentication method. It is deprecated + * and has been superseded by SASL Plain method. SASL Login uses a question and + * answer style communication. The server will request first the username and + * then the password. + * <p> + * Please note, that the password is not encrypted, it is only base64 encoded. + * Therefore it can be read or sniffed easily. A secure connection will solve + * this issue. So send whenever possible, a SieveStartTLSRequest before calling + * this request. + * <p> + * Client > AUTHENTICATE "LOGIN" | AUTHENTICATE "LOGIN" + * Server < {12} | {12} + * < VXNlcm5hbWU6 | Username: + * Client > {8+} | {8+} + * > Z2Vlaw== | geek + * Server < {12} | {12} + * < UGFzc3dvcmQ6 | Password: + * Client > {12+} | {12+} + * > dGgzZzMzazE= | th3g33k1 + * Server < OK | OK + * + * @deprecated + */ +class SieveSaslLoginRequest extends SieveAbstractSaslRequest { + + /** + * @inheritdoc + */ + constructor() { + super(); + this.response = new SieveSaslLoginResponse(); + } + + /** + * @inheritdoc + */ + async getNextRequest(builder) { + switch (this.response.getState()) { + case STATE_LOGIN_INITIALIZED: + return builder + .addLiteral("AUTHENTICATE") + .addQuotedString("LOGIN"); + case STATE_LOGIN_USERNAME: + return builder + .addQuotedBase64(this._username); + case STATE_LOGIN_PASSWORD: + return builder + .addQuotedBase64(this._password); + } + + throw new Error("Unknown state in SASL login"); + } + + /** + * @inheritdoc + */ + hasNextRequest() { + if (this.response.hasError()) + return false; + + if (this.response.getState() === STATE_LOGIN_COMPLETED) + return false; + + return true; + } + + /** + * @inheritdoc + */ + async onResponse(parser) { + await this.response.parse(parser); + + if (this.hasNextRequest()) + return this; + + return await(super.onResponse(this.response)); + } +} + +const SHA_STATE_FIRST_MESSAGE = 0; +const SHA_STATE_FINAL_MESSAGE = 1; +const SHA_STATE_EMPTY_MESSAGE = 2; +const SHA_STATE_COMPLETED = 4; + +/** + * This request implements an abstract base class for the "Salted Challenge Response Authentication + * Mechanism" (SCRAM). A SASL SCRAM-SHA-1 compatible implementation is mandatory + * for every manage sieve server. SASL SCRAM-SHA-1 supersedes DIGEST-MD5. + * + * @author Thomas Schmid + */ +class SieveAbstractSaslScramRequest extends SieveAbstractSaslRequest { + + /** + * @inheritdoc + */ + constructor() { + super(); + this.response = new SieveSaslScramShaResponse(); + } + + /** + * Checks if the request supports authorization + * + * @returns {boolean} + * true, as a scram request can always be used for a proxy authentication + */ + isAuthorizable() { + // overwrite the default as this mechanism support authorization + return true; + } + + /** + * Gets the SASL Mechanism name. + * @abstract + * + * @returns {string} + * the SASL Mechanism's unique it as string. + */ + getSaslName() { + throw new Error("Implement SASL Name"); + } + + /** + * Returns the crypto engine/provider which should be used for this request. + * @abstract + * + * @returns {SieveCrypto} + * the crypto engine + */ + getCrypto() { + throw new Error("Implement Crypto Method which returns a crypto provider"); + } + + /** + * Before sending the username to the server is needs to be normalized. + * The comma and the equal character have a special meaning and need to + * escaped. + * + * @param {string} username + * the username which should be escaped + * @returns {string} + * the escaped username + */ + normalizeUsername(username) { + + // Safe Chars: + // UTF8-char except "=" and "," + // 0x01-0x2B, 0x2D-0x3C, 0X3E-0x7F, + // UTF8-2, UTF8-3, UTF8-4 + + // Theoretically we should run SASLPrep on str but the server does ist anyway. + return username.replace("=", "=3D").replace(",", "=2C"); + } + + /** + * Calculates a nonce for the initial request. + * + * @returns {string} + * a sequence of printable ascii characters except a comma (,). + */ + async generateNonce() { + return await (this.getCrypto().H("" + (Math.random() * SEED), "hex")); + } + + /** + * Sends the "client-first-message" to the server. + * + * @param {SieveRequestBuilder} builder + * a reference to an request builder. + * + * @returns {SieveRequestBuilder} + * a builder which contains the request data. + */ + async sendFirstRequest(builder) { + + this._cnonce = await this.generateNonce(); + + // For integration tests, we need to fake the nonce... + // ... so we take the nonce from the rfc otherwise the verification fails. + // + // ### DEBUG SHA1 ### + // this._cnonce = "fyko+d2lbbFgONRv9qkxdawL"; + // this._username = "user"; + // this._password = "pencil"; + // this._authorization = ""; + // + // ### DEBUG SHA256 ### + // this._cnonce = "rOprNGfwEbeRWgbNEkqO"; + // this._username = "user"; + // this._password = "pencil"; + + this._authMessage = `n=${this.normalizeUsername(this._username)},r=${this._cnonce}`; + this._g2Header = `n,${(this._authorization !== "" ? "a=" + this.normalizeUsername(this._authorization) : "")},`; + + return builder + .addLiteral("AUTHENTICATE") + .addQuotedString(this.getSaslName()) + .addQuotedBase64(`${this._g2Header}${this._authMessage}`); + } + + /** + * Sends the "client-final-message" to the server. + * + * @param {SieveRequestBuilder} builder + * a reference to an request builder. + * + * @returns {SieveRequestBuilder} + * a builder which contains the request data. + */ + async sendFinalRequest(builder) { + + // Check if the server returned our nonce. This should prevent... + // ... man in the middle attacks. + const nonce = this.response.getNonce(); + if ((nonce.substr(0, this._cnonce.length) !== this._cnonce)) + throw new Error("Nonce invalid"); + + const crypto = this.getCrypto(); + + // As first step we need to salt the password... + const salt = this.response.getSalt(); + const iter = this.response.getIterationCounter(); + + const password = (new TextEncoder()).encode(this._password); + + // ... this is done by applying a simplified PBKDF2 algorithm... + // ... so we endup by calling Hi(Normalize(password), salt, i) + this._saltedPassword = await (crypto.Hi(crypto.normalize(password), salt, iter)); + + // the clientKey is defined as HMAC(SaltedPassword, "Client Key") + const clientKey = await (crypto.HMAC(this._saltedPassword, "Client Key")); + + // create the client-final-message-without-proof, ... + + const msg = `c=${(new SieveBase64Encoder(this._g2Header)).toUtf8()},r=${nonce}`; + // ... append it and the server-first-message to client-first-message-bare... + this._authMessage += "," + this.response.getServerFirstMessage() + "," + msg; + + // ... and convert it into a byte array. + this._authMessage = crypto.strToByteArray(this._authMessage); + + + // As next Step sign out message, this is done by applying the client... + // ... key through a pseudorandom function to the message. It is defined... + // as HMAC(H(ClientKey), AuthMessage) + const clientSignature = await (crypto.HMAC( + await (crypto.H(clientKey)), + this._authMessage)); + + // We now complete the cryptographic part an apply our clientkey to the... + // ... Signature, so that the server can be sure it is talking to us. + // The RFC defines this step as ClientKey XOR ClientSignature + const clientProof = clientKey; + for (let k = 0; k < clientProof.length; k++) + clientProof[k] ^= clientSignature[k]; + + // Every thing done so let's send the message... + // "c=" base64( (("" / "y") "," [ "a=" saslname ] "," ) "," "r=" c-nonce s-nonce ["," extensions] "," "p=" base64 + return builder + .addQuotedBase64(`${msg},p=${(new SieveBase64Encoder(clientProof)).toUtf8()}`); + // return "\""+btoa(msg+",p="+btoa(this.byteArrayToStr(clientProof)))+"\"\r\n"; + } + + /** + * @inheritdoc + */ + async getNextRequest(builder) { + + // Step1: Client sends Message to server. See SASL Login how to integrate it + // into the AUTHENTICATE Command. + // + // e.g.: "AUTHENTICATE \"SCRAM-SHA-1\" \"n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL\"\r\n" + + + switch (this.response.getState()) { + case SHA_STATE_FIRST_MESSAGE: + return await (this.sendFirstRequest(builder)); + case SHA_STATE_FINAL_MESSAGE: + return await (this.sendFinalRequest(builder)); + case SHA_STATE_EMPTY_MESSAGE: + // We have to send an empty response. The server did not wrap... + // ... the verifier into the Response Code... + return builder + .addQuotedString(""); + } + + throw new Error(`Illegal state ${this.response.getState()} in SASL SCRAM`); + } + + /** + * @inheritdoc + */ + hasNextRequest() { + + if (this.response.hasError()) + return false; + + if (this.response.getState() === SHA_STATE_COMPLETED) + return false; + + return true; + } + + /** + * @inheritdoc + */ + async onOk(response) { + + const crypto = this.getCrypto(); + + const serverSignature = await (crypto.HMAC( + await (crypto.HMAC(this._saltedPassword, "Server Key")), + this._authMessage + )); + + if (response.getVerifier() !== crypto.byteArrayToStr(serverSignature)) { + + response.setResponse(RESPONSE_NO); + response.setMessage("Server Signature not invalid"); + + await this.onNo(response); + return; + } + + await super.onOk(response); + } + + /** + * @inheritdoc + */ + async onResponse(parser) { + + await this.response.parse(parser); + + if (this.hasNextRequest()) + return this; + + return await(super.onResponse(this.response)); + } +} + +/** + * Implements the SCRAM-SHA-1 mechanism. + */ +class SieveSaslScramSha1Request extends SieveAbstractSaslScramRequest { + + /** + * @inheritdoc + */ + getSaslName() { + return "SCRAM-SHA-1"; + } + + /** + * @inheritdoc + */ + getCrypto() { + return new SieveCrypto("SHA-1"); + } +} + +/** + * Implements the SCRAM-SHA-256 mechanism. + */ +class SieveSaslScramSha256Request extends SieveAbstractSaslScramRequest { + + /** + * @inheritdoc + */ + getSaslName() { + return "SCRAM-SHA-256"; + } + + /** + * @inheritdoc + */ + getCrypto() { + return new SieveCrypto("SHA-256"); + } +} + +/** + * Implements the SCRAM-SHA-512 mechanism. + */ +class SieveSaslScramSha512Request extends SieveAbstractSaslScramRequest { + + /** + * @inheritdoc + */ + getSaslName() { + return "SCRAM-SHA-512"; + } + + /** + * @inheritdoc + */ + getCrypto() { + return new SieveCrypto("SHA-512"); + } +} + +/** + * This request implements SASL External Mechanism (rfc4422 Appendix A). + * It's a dumb-dumb implementation, and relies upon an established tls connection. + * It tells the server to use the cert provided during the TLS handshake. + * + * @author Thomas Schmid + */ +class SieveSaslExternalRequest extends SieveAbstractSaslRequest { + + /** + * @inheritdoc + */ + isAuthorizable() { + // overwrite the default behaviour. + return true; + } + + /** + * @inheritdoc + */ + async getNextRequest(builder) { + return builder + .addLiteral("AUTHENTICATE") + .addQuotedString("EXTERNAL") + .addQuotedBase64("" + this._authorization); + } + + /** + * SASL External uses the TLS Cert for authentication. + * Thus it does not rely upon any password, so this method returns always false. + * + * @returns {boolean} + * returns always false + */ + hasPassword() { + return false; + } + + /** + * @inheritdoc + */ + async onResponse(parser) { + return await(super.onResponse( + await (new SieveSimpleResponse()).parse(parser))); + } +} + +export { + SieveGetScriptRequest, + SievePutScriptRequest, + SieveCheckScriptRequest, + SieveSetActiveRequest, + SieveCapabilitiesRequest, + SieveDeleteScriptRequest, + SieveNoopRequest, + SieveRenameScriptRequest, + SieveListScriptsRequest, + SieveStartTLSRequest, + SieveLogoutRequest, + SieveInitRequest, + SieveSaslPlainRequest, + SieveSaslLoginRequest, + SieveSaslScramSha1Request, + SieveSaslScramSha256Request, + SieveSaslScramSha512Request, + SieveSaslExternalRequest +}; diff --git a/src/common/libManageSieve/SieveRequestBuilder.mjs b/src/common/libManageSieve/SieveRequestBuilder.mjs new file mode 100644 index 00000000..10ce2873 --- /dev/null +++ b/src/common/libManageSieve/SieveRequestBuilder.mjs @@ -0,0 +1,131 @@ +/* + * The contents of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via email + * from the author. Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +import { SieveBase64Encoder } from "./SieveBase64.mjs"; + +/** + * A helper class used to build standard compliant sieve requests. + */ +class SieveRequestBuilder { + + /** + * Creates a new instance + */ + constructor() { + this.data = ""; + } + + /** + * Adds a string as quoted base 64 encoded literal to the request. + * + * This is typically needed for SASL requests as they have to be + * base64 encoded by definition. + * + * @param {string} token + * the string which should be added to the request. + * @returns {SieveAbstractRequestBuilder} + * a self reference + */ + addQuotedBase64(token) { + if (token === undefined || token === null) + throw new Error("Invalid token"); + + this.addLiteral(`"${(new SieveBase64Encoder(token)).toUtf8()}"`); + return this; + } + + /** + * Adds a string as quoted literal to the request. + * + * This is typically used for string without a line break. + * In case you know you'll have a line break use the multiline + * version for better readability. + * + * Do not use this for any sasl method. All sasl strings + * have to be base 64 encoded. Refer to addQuotedBase64String instead. + * + * @param {string} [token] + * the string which should be added to the request. + * if omitted an empty string is sent. + * @returns {SieveAbstractRequestBuilder} + * a self reference + */ + addQuotedString(token) { + if (typeof (token) === "undefined" || token === null) + token = ""; + + this.addLiteral('"' + this.escapeString(token) + '"'); + return this; + } + + /** + * Adds a string as multiline literal to the request. + * + * It improves the requests readability in case you need to send a + * string containing a line break. + * + * @param {string} token + * the string which should be added to the request. + * @returns {SieveAbstractRequestBuilder} + * a self reference + */ + addMultiLineString(token) { + // Calculate the length in bytes + const length = (new TextEncoder()).encode(token).byteLength; + // return Buffer.byteLength(data, 'utf8'); + + this.addLiteral(`{${length}+}\r\n${token}`); + return this; + } + + /** + * Adds a literal to the request. + * The literal will used as it is. It will not be wrapped in a string or escaped. + * In case you need this use the specialized methods. + * + * @param {string} token + * the literal which should be added. + * @returns {SieveAbstractRequestBuilder} + * a self reference + */ + addLiteral(token) { + + if (this.data !== "") + this.data += " "; + + this.data += token; + return this; + } + + /** + * Returns the current request as it was cached and build up to the call. + * + * @returns {string} + * the current request including a trailing line break + */ + getBytes() { + return this.data + "\r\n"; + } + + /** + * Escapes a string. All Backslashes are converted to \\ while + * all quotes are escaped as \" + * + * @param {string} str + * the string which should be escaped + * @returns {string} + * the escaped string. + */ + escapeString(str) { + return str.replace(/\\/g, "\\\\").replace(/"/g, "\\\""); + } + +} + +export { SieveRequestBuilder }; diff --git a/src/common/libManageSieve/SieveResponse.js b/src/common/libManageSieve/SieveResponse.js deleted file mode 100755 index 06185b01..00000000 --- a/src/common/libManageSieve/SieveResponse.js +++ /dev/null @@ -1,1103 +0,0 @@ -/* - * The contents of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via email - * from the author. Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - * - * Contributors: - * Max Dittrich - */ - -(function (exports) { - - // Enable Strict Mode - "use strict"; - - const { - SieveResponseCode, - SieveResponseCodeSasl, - SieveResponseCodeReferral - } = require("./SieveResponseCodes.js"); - - const CHAR_LOWERCASE_B = 66; - const CHAR_UPPERCASE_B = 98; - const CHAR_B = [CHAR_LOWERCASE_B, CHAR_UPPERCASE_B]; - - const CHAR_LOWERCASE_E = 69; - const CHAR_UPPERCASE_E = 101; - const CHAR_E = [CHAR_LOWERCASE_E, CHAR_UPPERCASE_E]; - - const CHAR_LOWERCASE_N = 78; - const CHAR_UPPERCASE_N = 110; - const CHAR_N = [CHAR_LOWERCASE_N, CHAR_UPPERCASE_N]; - - const CHAR_LOWERCASE_O = 79; - const CHAR_UPPERCASE_O = 111; - const CHAR_O = [CHAR_LOWERCASE_O, CHAR_UPPERCASE_O]; - - const CHAR_LOWERCASE_K = 75; - const CHAR_UPPERCASE_K = 107; - const CHAR_K = [CHAR_LOWERCASE_K, CHAR_UPPERCASE_K]; - - const CHAR_LOWERCASE_Y = 89; - const CHAR_UPPERCASE_Y = 121; - const CHAR_Y = [CHAR_LOWERCASE_Y, CHAR_UPPERCASE_Y]; - - const CHAR_BRACKET_OPEN = 40; - const CHAR_BRACKET_CLOSE = 41; - const CHAR_SPACE = 32; - const CHAR_CR = 13; - - const TOKEN_OK = [CHAR_O, CHAR_K]; - const TOKEN_BYE = [CHAR_B, CHAR_Y, CHAR_E]; - const TOKEN_NO = [CHAR_N, CHAR_O]; - - const SIEVE_VERSION_1 = 1.0; - - const ONE_CHAR = 1; - - const RESPONSE_OK = 0; - const RESPONSE_BYE = 1; - const RESPONSE_NO = 2; - - const MAX_REDIRECTS_UNLIMITED = -1; - - /** - * This class implements a generic response handler for simple sieve requests. - * - * Simple requests just indicate, wether the command succeeded or not. They - * return only status information, and do not contain any data relevant for - * the user. - * - * @see SieveResponseParser - * - * @param {SieveResponseParser} [parser] - * a SieveResponseParser object containing the response sent by the server. - * - */ - class SieveSimpleResponse { - - /** - * Initializes the simple response object. - */ - constructor() { - this.message = null; - this.responseCode = null; - this.response = null; - } - - /** - * Parses the server's status response. It indicates if the command succeeded or failed. - * - * @param {SieveAbstractResponseParser} parser - * a SieveResponseParser object containing the response sent by the server. - * @returns {SieveSimpleResponse} - * a self reference - */ - parse(parser) { - /* - * Examples for simple responses - * - * 'NO (0000) "Message"\r\n' - * 'BYE (0000) {4+}\r\n1234\r\n' - * 'NO \"Message\"\r\n' - * 'BYE {4+}\r\n1234\r\n' - * 'NO (0000)\r\n' - */ - - this.message = ""; - this.responseCode = []; - - // OK - if (parser.startsWith(TOKEN_OK)) { - this.response = RESPONSE_OK; - parser.extract(TOKEN_OK.length); - } - // BYE - else if (parser.startsWith(TOKEN_BYE)) { - this.response = RESPONSE_BYE; - parser.extract(TOKEN_BYE.length); - } - // NO - else if (parser.startsWith(TOKEN_NO)) { - this.response = RESPONSE_NO; - parser.extract(TOKEN_NO.length); - } - else - throw new Error("NO, OK or BYE expected in " + parser.getData()); - - // is there a Message? - if (parser.isLineBreak()) { - parser.extractLineBreak(); - return this; - } - - // remove the space - parser.extractSpace(); - - // we found "(" so we got an responseCode, they are extremely ugly... - if (parser.startsWith([[CHAR_BRACKET_OPEN]])) { - // remove the opening bracket... - parser.extract(ONE_CHAR); - // ... but remember it - let nesting = 0; - - // According to the RFC the first tag must be always an atom, but in... - // ... reality this is not true. Cyrus servers send it as a string - if (parser.isString()) - this.responseCode.push(parser.extractString()); - else - this.responseCode.push(parser.extractToken([CHAR_SPACE, CHAR_BRACKET_CLOSE])); - - while (parser.isSpace()) { - parser.extractSpace(); - - // We might stumble upon opening brackets... - if (parser.startsWith([[CHAR_BRACKET_OPEN]])) { - // ... oh we did, so increase our nesting counter. - parser.extract(ONE_CHAR); - nesting++; - } - - // ok, more tokens, more fun... - // ... it could be either a string, a number, an atom or even a bracket - if (parser.isString()) - this.responseCode.push(parser.extractString()); - else - this.responseCode.push(parser.extractToken([CHAR_SPACE, CHAR_BRACKET_CLOSE])); - - // is it a closing bracket - if (parser.startsWith([[CHAR_BRACKET_CLOSE]]) && nesting) { - parser.extract(ONE_CHAR); - nesting--; - } - } - - if (!parser.startsWith([[CHAR_BRACKET_CLOSE]])) - throw new Error("Closing brackets expected in " + parser.getData()); - - parser.extract(ONE_CHAR); - - if (parser.isLineBreak()) { - parser.extractLineBreak(); - return this; - } - - parser.extractSpace(); - } - - this.message = parser.extractString(); - - parser.extractLineBreak(); - - return this; - } - - /** - * The server may return a human readable (error) message - * @returns {string} - * the human readable message - */ - getMessage() { - if ((typeof (this.message) === 'undefined') || (this.message === null)) - throw new Error("Message not Initialized"); - - return this.message; - } - - /** - * Checks if the request failed. In this case the server returns an error - * instead of the expected response. - * @returns {boolean} - * true in case the request succeeded, false in case it failed due to an error. - */ - hasError() { - if ((typeof (this.response) === 'undefined') || (this.response === null)) - throw new Error("response not Initialized"); - - if (this.response === RESPONSE_OK) - return false; - - return true; - } - - /** - * The server responds to a message with either an ok, bye or no. - * - * @returns {int} - * the servers response. It is set to 0 in case of an OK, to 1 in case of a BYE and to 3 incase of a NO - */ - getResponse() { - return this.response; - } - - /** - * A response code is used by the server to narrow down an error or to give hints. - * - * E.g.: In case the server wants to do a referral. It will answer a request with a BYE - * and adds the details to the REFERRAL response code. - * - * Or in case the user tries do delete the active script. Then the server responds with - * a NO and will an response code "ACTIVE". - * - * In case of putting a script to the server it may respond with an OK and a WARNING - * response code. Which means the script contains warnings which should addressed by the - * server. - * - * @returns {SieveResponseCode} - * the response code for the current request. - */ - getResponseCode() { - if ((typeof (this.responseCode) === 'undefined') || (this.responseCode === null)) - throw new Error("Response Code not Initialized"); - - let code = ""; - if (this.responseCode.length) - code = this.responseCode[0].toUpperCase(); - - switch (code) { - case "REFERRAL": - return new SieveResponseCodeReferral(this.responseCode); - - case "SASL": - return new SieveResponseCodeSasl(this.responseCode); - } - - // TODO Implement all of the Response codes: - // "ACTIVE" / "NONEXISTENT" / "ALREADYEXISTS" / "WARNINGS" /AUTH-TOO-WEAK /TRANSITION-NEEDED /TRYLATER/ ENCRYPT-NEEDED / QUOTA / TAG - return new SieveResponseCode(this.responseCode); - } - } - - /** - * Parses the capabilities posted by the ManageSieve server upon a client - * connection, after successful STARTTLS and AUTHENTICATE or by issuing the - * CAPABILITY command. - * - * @see {SieveCapabilitiesRequest} - * - * @param {SieveResponseParser} parser - * a parser containing the response sent by the server - */ - class SieveCapabilitiesResponse extends SieveSimpleResponse { - - /** - * @inheritdoc - */ - constructor() { - - super(); - - this.details = { - implementation: null, - version: 0, - - extensions: {}, - tls: false, - sasl: {}, - - maxredirects: MAX_REDIRECTS_UNLIMITED, - owner: "", - notify: {}, - language: "i-default", - - compatibility: {} - }; - } - - /** - * Parses the sieve extensions string. It is a space separated list of strings. - * @param {string} value - * the string which should be parsed - * @returns {object.<string, boolean>} - * a map with pairs of extension name and activation status. - */ - parseSieveExtensions(value) { - const extensions = value.split(" "); - const result = {}; - - for (let i = 0; i < extensions.length; ++i) - result["" + extensions[i]] = true; - - return result; - } - - /** - * @inheritdoc - */ - parse(parser) { - while (parser.isString()) { - const tag = parser.extractString(); - - let value = ""; - if (parser.isLineBreak() === false) { - parser.extractSpace(); - value = parser.extractString(); - } - - parser.extractLineBreak(); - - switch (tag.toUpperCase()) { - case "STARTTLS": - this.details.tls = true; - break; - case "IMPLEMENTATION": - this.details.implementation = value; - break; - case "SASL": - this.details.sasl = value.split(" "); - break; - case "SIEVE": - this.details.extensions = this.parseSieveExtensions(value); - break; - case "VERSION": - this.details.version = parseFloat(value); - if (this.details.version < SIEVE_VERSION_1) - break; - - // Version 1.0 introduced rename, noop and checkscript - this.details.compatibility.renamescript = true; - this.details.compatibility.noop = true; - this.details.compatibility.checkscript = true; - - break; - case "MAXREDIRECTS": - this.details.maxredirects = parseInt(value, 10); - break; - case "LANGUAGE": - this.details.language = value; - break; - case "NOTIFY": - this.details.notify = value.split(" "); - break; - case "OWNER": - this.details.owner = value; - break; - case "RENAME": - this.details.compatibility.renamescript = true; - break; - case "NOOP": - this.details.compatibility.noop = true; - break; - } - } - - if (this.details.implementation === null) - throw new Error("Server did not provide an Implementation string."); - - return super.parse(parser); - } - - /** - * Returns a structure which contains all the details on the server's capabilities - * like the implementation, version, extension, sasl mechanisms etc. - * - * @returns {object} - * the object which the capabilities. - */ - getDetails() { return this.details; } - - /** - * Returns the servers implementation details. - * - * This is a custom string which typically identifies the server's implementation - * as well a the version. - * - * You should never attempt to parse this string. - * - * @returns {string} - * the servers implementation details - */ - getImplementation() { return this.details.implementation; } - - /** - * Returns the list of supported sasl mechanisms. - * - * They may change after a secure channel was established. - * @returns {string} - * the sasl mechanism - */ - getSasl() { - return this.details.sasl; - } - - /** - * Returns the server's supported sieve language extensions - * - * @param {boolean} [asString] - * optional if true a string will be returned otherwise a - * structure with key value pairs. - * - * @returns {object.<string,boolean>|string} - * the server's supported extension. - */ - getExtensions(asString) { - if (!asString) - return this.details.extensions; - - let result = ""; - - for (const item in this.details.extensions) - result += item + " "; - - return result; - } - - /** - * Indicates wether or not TLS is supported by this implementation. - * - * Note: After the command STARTTLS or AUTHENTICATE completes successfully, this - * value is always false. - * - * @returns {boolean} - * true if TLS is supported, false if not. - */ - getTLS() { return this.details.tls; } - - /** - * In order to maintain compatibility to older implementations, the servers - * should state their compatibility level upon login. - * - * A value of "0" indicates, minimal ManageSieve support. This means the server - * implements the commands AUTHENTICATE, STARTTLS, LOGOUT, CAPABILITY, HAVESPACE, - * PUTSCRIPT, LISTSCRIPTS, SETACTIVE, GETSCRIPT and DELETESCRIPT - * - * A value of "1.0" adds to the minimal ManageSieve Support the commands - * RENAMESCRIPT, CHECKSCRIPT and NOOP. - * - * @returns {float} - * a positive float describing the compatibility level of the ManageSieve server. - */ - getVersion() { return this.details.version; } - - /** - * Returns the limit on the number of Sieve "redirect" actions a script can - * perform during a single evaluation. - * - * Note, this is different from the total number of "redirect" actions a - * script can contain. - * - * @returns {int} - * a non-negative number of redirects, or -1 for infinite redirects - */ - getMaxRedirects() { return this.details.maxredirects; } - - /** - * Returns a string array of URI schema parts for supported notification - * methods. This capability is be specified, if the Sieve implementation - * supports the "enotify" extension. - * - * @returns {string[]} - * The schema parts as string array - */ - getNotify() { return this.details.notify; } - - /** - * Returns the language currently used for human readable error messages. - * If this capability is not returned, the "i-default" [RFC2277] language is - * assumed. - * - * Note that the current language might be per-user configurable (i.e. it - * might change after authentication) - * - * @returns {string} - * a [RFC4646] conform language tag as string - */ - getLanguage() { return this.details.language; } - - /** - * Returns a list with sieve commands which are supported by this implementation - * and are not part of the absolute minimal ManageSieve support. - * - * The server advertises such additional commands either by explicitly - * naming the command or by using the compatibility level capability. - * - * Examples are RENAME, NOOP and CHECKSCRIPT. - * - * @returns {object} - * an associative array containing additional sieve commands - */ - getCompatibility() { return this.details.compatibility; } - - /** - * Gets the name of the logged in user. - * - * Note: This value is only available after AUTHENTICATE command succeeds - * - * @returns {string} - * a String containing the username - */ - getOwner() { return this.details.owner; } - } - - - - /** - * Parses list script response. - */ - class SieveListScriptsResponse extends SieveSimpleResponse { - - /** - * @inheritdoc - */ - parse(parser) { - // sieve-name = string - // string = quoted / literal - // (sieve-name [SP "ACTIVE"] CRLF) response-oknobye - - const scripts = []; - let i = -1; - - while (parser.isString()) { - i++; - - scripts[i] = {}; - scripts[i].script = parser.extractString(); - - if (parser.isLineBreak()) { - scripts[i].active = false; - parser.extractLineBreak(); - - continue; - } - - parser.extractSpace(); - - if (parser.extractToken([CHAR_CR]).toUpperCase() !== "ACTIVE") - throw new Error("Error \"ACTIVE\" expected"); - - scripts[i].active = true; - parser.extractLineBreak(); - - } - - this.scripts = scripts; - - return super.parse(parser); - } - - - /** - * An array of objects. Each object represents a script and - * has at least a property named script which contains the - * script name and a property named active which is either - * true or false. - * - * @returns {object[]} - * an array of objects with the name and activation state for each script - */ - getScripts() { - return this.scripts; - } - } - - /** - * Parses a get script response which returns the content - * of a script - */ - class SieveGetScriptResponse extends SieveSimpleResponse { - - /** - * Parses a get script response. - * - * It is perfectly fine for the server to return an empty script. - * - * @param {string} name - * the script name, to simplify the handling as the server just returns the content. - */ - constructor(name) { - super(); - this.scriptName = name; - } - - /** - * @inheritdoc - */ - parse(parser) { - let body = ""; - // [(<"> *1024QUOTED-CHAR <">) / ("{" number "+}" CRLF *OCTET) CRLF] response-oknobye - if (parser.isString()) { - body = parser.extractString(); - parser.extractLineBreak(); - } - - this.scriptBody = body; - - return super.parse(parser); - } - - /** - * Contains the requested sieve script. - * Keep in mind scripts can't be locked, so several clients may manipulate - * a script at the same time. - * - * @returns {string} returns the requested script's content - */ - getScriptBody() { return this.scriptBody; } - - /** - * Returns the requested script's name, - * - * @returns {string} the script name. - */ - getScriptName() { return this.scriptName; } - } - - - /** - * In contrast to a simple sieve request most of the sasl - * requests are more complex and require multiple round trips - * to be completed. Which means the response has to track - * the state. - * - * This simple wrapper makes the response stateful. - */ - class SieveStateFullResponse extends SieveSimpleResponse { - - /** - * @inheritdoc - */ - constructor() { - super(); - this.state = 0; - } - - /** - * Gets the responses current state - * - * @returns {int} - * the current state as integer - */ - getState() { return this.state; } - - } - - const STATE_LOGIN_USERNAME = 0; - const STATE_LOGIN_PASSWORD = 1; - const STATE_LOGIN_VERIFICATION = 2; - const STATE_LOGIN_COMPLETED = 4; - - /** - * SASL Login responses consist of multiple responses and requests. - * - * This means you need to call the parse method unless you reach the desired - * state in the state engine or unless an exception is thrown. - * - * The state can be retrieved by calling getState. - */ - class SieveSaslLoginResponse extends SieveStateFullResponse { - - /** - * @inheritdoc - */ - parse(parser) { - - if ((this.state === STATE_LOGIN_USERNAME) && (parser.isString())) { - // String should be 'Username:' or something similar - parser.extractString(); - parser.extractLineBreak(); - - this.state = STATE_LOGIN_PASSWORD; - return this; - } - - if ((this.state === STATE_LOGIN_PASSWORD) && (parser.isString())) { - // String should be equivalent to 'Password:' - parser.extractString(); - parser.extractLineBreak(); - - this.state = STATE_LOGIN_VERIFICATION; - return this; - } - - if (this.state === STATE_LOGIN_VERIFICATION) { - // Should be either a NO, BYE or OK - this.state = STATE_LOGIN_COMPLETED; - - super.parse(parser); - return this; - } - - // is it an error message? - try { - super.parse(parser); - } - catch (ex) { - throw new Error(`Illegal State: ${this.state} / ${parser.getData(0)}\n${ex}`); - } - - this.state = STATE_LOGIN_COMPLETED; - return this; - } - } - - const STATE_CRAMMD5_INITIATED = 0; - const STATE_CRAMMD5_CHALLENGED = 1; - const STATE_CRAMMD5_COMPLETED = 4; - - /** - * @author Thomas Schmid - * @author Max Dittrich - */ - class SieveSaslCramMd5Response extends SieveStateFullResponse { - - /** - * @inheritdoc - */ - parse(parser) { - - if ((this.state === STATE_CRAMMD5_INITIATED) && (parser.isString())) { - // The challenge is contained within a string - this.challenge = parser.extractString(); - parser.extractLineBreak(); - - this.state = STATE_CRAMMD5_CHALLENGED; - - return this; - } - - if (this.state === STATE_CRAMMD5_CHALLENGED) { - // Should be either a NO, BYE or OK - this.state = STATE_CRAMMD5_COMPLETED; - - // Invoke the parent parser to consume the rest of the message - super.parse(parser); - return this; - } - - throw new Error(`Illegal State: ${this.state} / ${parser.getData()}`); - } - - /** - * Gets the challenge returned by the server. - * - * @returns {string} - * the server's challenge which needs to be answered. - */ - getChallenge() { - if (this.state < STATE_CRAMMD5_CHALLENGED) - throw new Error("Illegal State, request not completed"); - - return this.challenge; - } - } - - const SHA_STATE_FIRST_MESSAGE = 0; - const SHA_STATE_FINAL_MESSAGE = 1; - const SHA_STATE_COMPLETED = 4; - - const SHA_FIRST_TOKEN = 0; - const SHA_PREFIX_LENGTH = 2; - - /** - * Parses responses for SCRAM-SHA authentication. - * - * SCRAM is a secure client first authentication mechanism. The client - * challenges the server and deicides if the connection is trustworthy. - * - * This requires a way mor logic on the client than with simple authentication - * mechanisms. It also requires more communication, in total two roundtrips. - */ - class SieveSaslScramShaResponse extends SieveStateFullResponse { - - /** - * Extracts the reserved-mext token from the array of tokens. - * @param {string[]} tokens - * the first message response split into tokens. - * @returns {string} - * the optional reserved-mext token or an empty string. - */ - _extractReservedMext(tokens) { - const token = tokens[SHA_FIRST_TOKEN]; - - // Test for the reserved-mext token. If it is existent, we just skip it - if ((token.length <= SHA_PREFIX_LENGTH) || !token.startsWith("m=")) - return ""; - - tokens.shift(); - return token.substr(SHA_FIRST_TOKEN); - } - - /** - * Extracts the nonce from the first message. - * @param {string[]} tokens - * the first message response split into tokens. - * @returns {string} - * the nonce or an exception in case it could not be extracted. - */ - _extractNonce(tokens) { - const token = tokens[SHA_FIRST_TOKEN]; - - // Extract the nonce - if ((token.length <= SHA_PREFIX_LENGTH) || !token.startsWith("r=")) - throw new Error(`Nonce expected but got ${token}`); - - tokens.shift(); - - // remove the "r=" - return token.substr(SHA_PREFIX_LENGTH); - } - - /** - * Extracts the salt from the first message. - * @param {string[]} tokens - * the first message response split into tokens. - * @returns {string} - * the salt as base64 encoded string or an exception in case - * it could not be extracted - */ - _extractSalt(tokens) { - const token = tokens[SHA_FIRST_TOKEN]; - - if ((token.length <= SHA_PREFIX_LENGTH) || !token.startsWith("s=")) - throw new Error(`Salt expected but got ${token}`); - - tokens.shift(); - - // remove the "s="" - return token.substr(SHA_PREFIX_LENGTH); - } - - /** - * Extracts the iteration count from the first message. - * @param {string[]} tokens - * the first message response split into tokens. - * @returns {int} - * the iteration count as integer or an exception in case the - * iterations could not be extracted. - */ - _extractIterations(tokens) { - const token = tokens[SHA_FIRST_TOKEN]; - - if ((token.length <= SHA_PREFIX_LENGTH) || !token.startsWith("i=")) - throw new Error(`Iteration Count expected but got ${token}`); - - tokens.shift(); - - // Remove the prefix and convert to an integer - return parseInt(token.substr(SHA_PREFIX_LENGTH), 10); - } - - /** - * Parses the server-first-message it is defined to be: - * [reserved-mext ","] nonce "," salt "," iteration-count ["," extensions] - * - * Where - * reserved-mext : "m=" 1*(value-char) - * nonce : "r=" c-nonce - * salt : "s=" base64(salt) - * iteration-count : "i=" posit-number - * - * Extensions are optional and for future use. - * Neither c-nonce nor salt can contain a "," character - * - * @param {SieveResponseParser} parser - * the response parser which contains the response to be parsed. - * - * - * @private - */ - _parseFirstMessage(parser) { - this._serverFirstMessage = parser.convertFromBase64(parser.extractString()); - - const tokens = this._serverFirstMessage.split(','); - - this._extractReservedMext(tokens); - this._nonce = this._extractNonce(tokens); - - this._salt = parser.convertFromBase64(this._extractSalt(tokens)); - this._iter = this._extractIterations(tokens); - } - - /** - * Parses the server-final-message. - * - * It is defined to be: - * (server-error / verifier) ["," extensions] - * - * Where - * server-error : "e=" server-error-value - * verifier : "v=" base64(ServerSignature) - * - * Extensions are optional and for future use. - * As suggested by the RFC they will be ignored - * - * @param {SieveResponseParser} parser - * the parser which should be to process the message. - * @param {string} [data] - * optional, the server's final message. It omitted it - * will be parsed from the response. - * - * - * @private - */ - _parseFinalMessage(parser, data) { - - if (typeof (data) === "undefined" || data === null) - data = parser.extractString(); - - // server-final-message = (server-error / verifier) ["," extensions] - const token = parser.convertFromBase64(data).split(",")[SHA_FIRST_TOKEN]; - - if (token.length <= SHA_PREFIX_LENGTH) - throw new Error(`Response expected but got: ${data}`); - - // server-error = "e=" - if (token.startsWith("e=")) { - this._serverError = token.substr(SHA_PREFIX_LENGTH); - return; - } - - // verifier = "v=" base64 - if (token.startsWith("v=")) { - this._verifier = parser.convertFromBase64(token.substr(SHA_PREFIX_LENGTH)); - return; - } - - throw new Error("Invalid Final message"); - } - - /** - * @inheritdoc - */ - parse(parser) { - - if ((this.state === SHA_STATE_FIRST_MESSAGE) && (parser.isString())) { - this._parseFirstMessage(parser); - parser.extractLineBreak(); - - this.state = SHA_STATE_FINAL_MESSAGE; - - return this; - } - - - // There are two valid responses... - // ... either the Server sends us something like that: - // - // S: cnNwYXV0aD1lYTQwZjYwMzM1YzQyN2I1NTI3Yjg0ZGJhYmNkZmZmZA== - // C: "" - // S: OK - - if ((this.state === SHA_STATE_FINAL_MESSAGE) && (parser.isString())) { - - this._parseFinalMessage(parser); - parser.extractLineBreak(); - - this.state = 2; - - return this; - } - - if (this.state === 2) { - super.parse(parser); - this.state = SHA_STATE_COMPLETED; - return this; - } - - // Or the response is wrapped into the ResponseCode in order to save... - // ... roundtrip time so we end up with the following - // - // S: OK (SASL "cnNwYXV0aD1lYTQwZjYwMzM1YzQyN2I1NTI3Yjg0ZGJhYmNkZmZmZA==") - - if (this.state === SHA_STATE_FINAL_MESSAGE) { - super.parse(parser); - - this._parseFinalMessage(parser, this.getResponseCode().getSasl(), parser); - - this.state = SHA_STATE_COMPLETED; - - return this; - } - - throw new Error(`Illegal State: ${this.state} / ${parser.getData()}`); - } - - /** - * The salt is transferred with the first message and used to - * randomize the SHA request - * - * @returns {string} - * the salt - */ - getSalt() { - if (this.state < SHA_STATE_FINAL_MESSAGE) - throw new Error("Illegal State, request not completed"); - - return this._salt; - } - - /** - * @returns {int} - * the number of iterations. - */ - getIterationCounter() { - if (this.state < SHA_STATE_FINAL_MESSAGE) - throw new Error("Illegal State, request not completed"); - - return this._iter; - } - - /** - * Returns the nonce. Please note this it is only available after the - * final message was received - * - * @returns {string} - * the nonce received from the server on an exception in case it is unknown. - */ - getNonce() { - if (this.state < SHA_STATE_FINAL_MESSAGE) - throw new Error("Illegal State, request not completed"); - - return this._nonce; - } - - /** - * Returns the servers first message. Pleas not is tis only available when the - * final message was received. - * - * @returns {string} - * the server's first message or an exception in case it is unknown. - */ - getServerFirstMessage() { - if (this.state < SHA_STATE_FINAL_MESSAGE) - throw new Error("Illegal State, request not completed"); - - return this._serverFirstMessage; - } - - /** - * - */ - getServerError() { - if (this.state < 2) - throw new Error("Illegal State, request not completed"); - - return this._serverError; - } - - /** - * The server's signature which needs to be verified. - * - * @returns {string} - * the server's signature - */ - getVerifier() { - if (this.state < 2) - throw new Error("Illegal State, request not completed"); - - return this._verifier; - } - } - - exports.SieveSimpleResponse = SieveSimpleResponse; - exports.SieveCapabilitiesResponse = SieveCapabilitiesResponse; - exports.SieveListScriptsResponse = SieveListScriptsResponse; - exports.SieveSaslLoginResponse = SieveSaslLoginResponse; - exports.SieveSaslCramMd5Response = SieveSaslCramMd5Response; - exports.SieveGetScriptResponse = SieveGetScriptResponse; - exports.SieveSaslScramShaResponse = SieveSaslScramShaResponse; - -})(module.exports || this); diff --git a/src/common/libManageSieve/SieveResponse.mjs b/src/common/libManageSieve/SieveResponse.mjs new file mode 100644 index 00000000..6af83afe --- /dev/null +++ b/src/common/libManageSieve/SieveResponse.mjs @@ -0,0 +1,1103 @@ +/* + * The contents of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via email + * from the author. Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + * + * Contributors: + * Max Dittrich + */ + +import { + SieveResponseCode, + SieveResponseCodeSasl, + SieveResponseCodeReferral +} from "./SieveResponseCodes.mjs"; + +import { SieveBase64Decoder } from "./SieveBase64.mjs"; + +const CHAR_LOWERCASE_B = 66; +const CHAR_UPPERCASE_B = 98; +const CHAR_B = [CHAR_LOWERCASE_B, CHAR_UPPERCASE_B]; + +const CHAR_LOWERCASE_E = 69; +const CHAR_UPPERCASE_E = 101; +const CHAR_E = [CHAR_LOWERCASE_E, CHAR_UPPERCASE_E]; + +const CHAR_LOWERCASE_N = 78; +const CHAR_UPPERCASE_N = 110; +const CHAR_N = [CHAR_LOWERCASE_N, CHAR_UPPERCASE_N]; + +const CHAR_LOWERCASE_O = 79; +const CHAR_UPPERCASE_O = 111; +const CHAR_O = [CHAR_LOWERCASE_O, CHAR_UPPERCASE_O]; + +const CHAR_LOWERCASE_K = 75; +const CHAR_UPPERCASE_K = 107; +const CHAR_K = [CHAR_LOWERCASE_K, CHAR_UPPERCASE_K]; + +const CHAR_LOWERCASE_Y = 89; +const CHAR_UPPERCASE_Y = 121; +const CHAR_Y = [CHAR_LOWERCASE_Y, CHAR_UPPERCASE_Y]; + +const CHAR_BRACKET_OPEN = 40; +const CHAR_BRACKET_CLOSE = 41; +const CHAR_SPACE = 32; +const CHAR_CR = 13; + +const TOKEN_OK = [CHAR_O, CHAR_K]; +const TOKEN_BYE = [CHAR_B, CHAR_Y, CHAR_E]; +const TOKEN_NO = [CHAR_N, CHAR_O]; + +const SIEVE_VERSION_1 = 1; + +const ONE_CHAR = 1; + +const RESPONSE_UNKNOWN = -1; +const RESPONSE_OK = 0; +const RESPONSE_BYE = 1; +const RESPONSE_NO = 2; + +const MAX_REDIRECTS_UNLIMITED = -1; + +/** + * This class implements a generic response handler for simple sieve requests. + * + * Simple requests just indicate, wether the command succeeded or not. They + * return only status information, and do not contain any data relevant for + * the user. + * + * @see SieveResponseParser + * + * @param {SieveResponseParser} [parser] + * a SieveResponseParser object containing the response sent by the server. + * + */ +class SieveSimpleResponse { + + /** + * Initializes the simple response object. + */ + constructor() { + this.message = null; + this.responseCode = null; + this.response = RESPONSE_UNKNOWN; + } + + /** + * Parses the server's status response. It indicates if the command succeeded or failed. + * + * @param {SieveResponseParser} parser + * a SieveResponseParser object containing the response sent by the server. + * @returns {SieveSimpleResponse} + * a self reference + */ + parse(parser) { + /* + * Examples for simple responses + * + * 'NO (0000) "Message"\r\n' + * 'BYE (0000) {4+}\r\n1234\r\n' + * 'NO \"Message\"\r\n' + * 'BYE {4+}\r\n1234\r\n' + * 'NO (0000)\r\n' + */ + + this.setMessage(""); + this.responseCode = []; + + // OK + if (parser.startsWith(TOKEN_OK)) { + this.setResponse(RESPONSE_OK); + parser.extract(TOKEN_OK.length); + } + // BYE + else if (parser.startsWith(TOKEN_BYE)) { + this.setResponse(RESPONSE_BYE); + parser.extract(TOKEN_BYE.length); + } + // NO + else if (parser.startsWith(TOKEN_NO)) { + this.setResponse(RESPONSE_NO); + parser.extract(TOKEN_NO.length); + } + else + throw new Error("NO, OK or BYE expected in " + parser.getData()); + + // is there a Message? + if (parser.isLineBreak()) { + parser.extractLineBreak(); + return this; + } + + // remove the space + parser.extractSpace(); + + // we found "(" so we got an responseCode, they are extremely ugly... + if (parser.startsWith([[CHAR_BRACKET_OPEN]])) { + // remove the opening bracket... + parser.extract(ONE_CHAR); + // ... but remember it + let nesting = 0; + + // According to the RFC the first tag must be always an atom, but in... + // ... reality this is not true. Cyrus servers send it as a string + if (parser.isString()) + this.responseCode.push(parser.extractString()); + else + this.responseCode.push(parser.extractToken([CHAR_SPACE, CHAR_BRACKET_CLOSE])); + + while (parser.isSpace()) { + parser.extractSpace(); + + // We might stumble upon opening brackets... + if (parser.startsWith([[CHAR_BRACKET_OPEN]])) { + // ... oh we did, so increase our nesting counter. + parser.extract(ONE_CHAR); + nesting++; + } + + // ok, more tokens, more fun... + // ... it could be either a string, a number, an atom or even a bracket + if (parser.isString()) + this.responseCode.push(parser.extractString()); + else + this.responseCode.push(parser.extractToken([CHAR_SPACE, CHAR_BRACKET_CLOSE])); + + // is it a closing bracket + if (parser.startsWith([[CHAR_BRACKET_CLOSE]]) && nesting) { + parser.extract(ONE_CHAR); + nesting--; + } + } + + if (!parser.startsWith([[CHAR_BRACKET_CLOSE]])) + throw new Error("Closing brackets expected in " + parser.getData()); + + parser.extract(ONE_CHAR); + + if (parser.isLineBreak()) { + parser.extractLineBreak(); + return this; + } + + parser.extractSpace(); + } + + this.setMessage(parser.extractString()); + parser.extractLineBreak(); + + return this; + } + + /** + * The server may return a human readable (error) message + * + * @returns {string} + * the human readable message + */ + getMessage() { + if ((typeof (this.message) === 'undefined') || (this.message === null)) + throw new Error("Message not initialized"); + + return this.message; + } + + /** + * Set or changes the server's response message. + * + * @param {string} [message] + * the optional message + * + * @returns {SieveSimpleResponse} + * a self reference + */ + setMessage(message) { + this.message = message; + return this; + } + + /** + * Checks if the request failed. In this case the server returns an error + * instead of the expected response. + * + * @returns {boolean} + * true in case the request succeeded, false in case it failed due to an error. + */ + hasError() { + if (this.response === RESPONSE_UNKNOWN) + return false; + + if (this.response === RESPONSE_OK) + return false; + + return true; + } + + /** + * The server responds to a message with either an ok, bye or no. + * + * @returns {int} + * the servers response. It is set to 0 in case of an OK, to 1 in case of a BYE and to 3 incase of a NO + */ + getResponse() { + if (this.response === RESPONSE_UNKNOWN) + throw new Error("Response not initialized"); + + return this.response; + } + + /** + * Set or changes the server's response. + * + * @param {int} response + * the server's response code + * + * @returns {SieveSimpleResponse} + * a self reference + */ + setResponse(response) { + this.response = response; + return this; + } + + /** + * A response code is used by the server to narrow down an error or to give hints. + * + * E.g.: In case the server wants to do a referral. It will answer a request with a BYE + * and adds the details to the REFERRAL response code. + * + * Or in case the user tries do delete the active script. Then the server responds with + * a NO and will an response code "ACTIVE". + * + * In case of putting a script to the server it may respond with an OK and a WARNING + * response code. Which means the script contains warnings which should addressed by the + * server. + * + * @returns {SieveResponseCode} + * the response code for the current request. + */ + getResponseCode() { + if ((typeof (this.responseCode) === 'undefined') || (this.responseCode === null)) + throw new Error("Response Code not Initialized"); + + let code = ""; + if (this.responseCode.length) + code = this.responseCode[0].toUpperCase(); + + switch (code) { + case "REFERRAL": + return new SieveResponseCodeReferral(this.responseCode); + + case "SASL": + return new SieveResponseCodeSasl(this.responseCode); + } + + // TODO Implement all of the Response codes: + // "ACTIVE" / "NONEXISTENT" / "ALREADYEXISTS" / "WARNINGS" /AUTH-TOO-WEAK /TRANSITION-NEEDED /TRYLATER/ ENCRYPT-NEEDED / QUOTA / TAG + return new SieveResponseCode(this.responseCode); + } +} + +/** + * Parses the capabilities posted by the ManageSieve server upon a client + * connection, after successful STARTTLS and AUTHENTICATE or by issuing the + * CAPABILITY command. + * + * @see {SieveCapabilitiesRequest} + * + * @param {SieveResponseParser} parser + * a parser containing the response sent by the server + */ +class SieveCapabilitiesResponse extends SieveSimpleResponse { + + /** + * @inheritdoc + */ + constructor() { + + super(); + + this.details = { + implementation: null, + version: 0, + + extensions: {}, + tls: false, + sasl: {}, + + maxredirects: MAX_REDIRECTS_UNLIMITED, + owner: "", + notify: {}, + language: "i-default", + + compatibility: {} + }; + } + + /** + * Parses the sieve extensions string. It is a space separated list of strings. + * @param {string} value + * the string which should be parsed + * @returns {object.<string, boolean>} + * a map with pairs of extension name and activation status. + */ + parseSieveExtensions(value) { + const extensions = value.split(" "); + const result = {}; + + for (let i = 0; i < extensions.length; ++i) + result["" + extensions[i]] = true; + + return result; + } + + /** + * @inheritdoc + */ + parse(parser) { + + while (parser.isString()) { + const tag = parser.extractString(); + + let value = ""; + if (parser.isLineBreak() === false) { + parser.extractSpace(); + value = parser.extractString(); + } + + parser.extractLineBreak(); + + switch (tag.toUpperCase()) { + case "STARTTLS": + this.details.tls = true; + break; + case "IMPLEMENTATION": + this.details.implementation = value; + break; + case "SASL": + this.details.sasl = value.split(" "); + break; + case "SIEVE": + this.details.extensions = this.parseSieveExtensions(value); + break; + case "VERSION": + this.details.version = Number.parseFloat(value); + if (this.details.version < SIEVE_VERSION_1) + break; + + // Version 1.0 introduced rename, noop and checkscript + this.details.compatibility.renamescript = true; + this.details.compatibility.noop = true; + this.details.compatibility.checkscript = true; + + break; + case "MAXREDIRECTS": + this.details.maxredirects = Number.parseInt(value, 10); + break; + case "LANGUAGE": + this.details.language = value; + break; + case "NOTIFY": + this.details.notify = value.split(" "); + break; + case "OWNER": + this.details.owner = value; + break; + case "RENAME": + this.details.compatibility.renamescript = true; + break; + case "NOOP": + this.details.compatibility.noop = true; + break; + } + } + + super.parse(parser); + + // IMPLEMENTATION and SIEVE are mandatory fields in case of an OK Response + // But they do not exist in case of a BYE or NO. + if (this.hasError()) + return this; + + if (this.details.implementation === null) + throw new Error("Invalid capability response, no key named IMPLEMENTATION."); + + if (this.details.sieve === null) + throw new Error("Invalid capability response, no key named SIEVE."); + + return this; + } + + /** + * Returns a structure which contains all the details on the server's capabilities + * like the implementation, version, extension, sasl mechanisms etc. + * + * @returns {object} + * the object which the capabilities. + */ + getDetails() { return this.details; } + + /** + * Returns the servers implementation details. + * + * This is a custom string which typically identifies the server's implementation + * as well a the version. + * + * You should never attempt to parse this string. + * + * @returns {string} + * the servers implementation details + */ + getImplementation() { return this.details.implementation; } + + /** + * Returns the list of supported sasl mechanisms. + * + * They may change after a secure channel was established. + * @returns {string} + * the sasl mechanism + */ + getSasl() { + return this.details.sasl; + } + + /** + * Returns the server's supported sieve language extensions + * + * @param {boolean} [asString] + * optional if true a string will be returned otherwise a + * structure with key value pairs. + * + * @returns {object.<string,boolean>|string} + * the server's supported extension. + */ + getExtensions(asString) { + if (!asString) + return this.details.extensions; + + let result = ""; + + for (const item in this.details.extensions) + result += item + " "; + + return result; + } + + /** + * Indicates wether or not TLS is supported by this implementation. + * + * Note: After the command STARTTLS or AUTHENTICATE completes successfully, this + * value is always false. + * + * @returns {boolean} + * true if TLS is supported, false if not. + */ + getTLS() { return this.details.tls; } + + /** + * In order to maintain compatibility to older implementations, the servers + * should state their compatibility level upon login. + * + * A value of "0" indicates, minimal ManageSieve support. This means the server + * implements the commands AUTHENTICATE, STARTTLS, LOGOUT, CAPABILITY, HAVESPACE, + * PUTSCRIPT, LISTSCRIPTS, SETACTIVE, GETSCRIPT and DELETESCRIPT + * + * A value of "1.0" adds to the minimal ManageSieve Support the commands + * RENAMESCRIPT, CHECKSCRIPT and NOOP. + * + * @returns {float} + * a positive float describing the compatibility level of the ManageSieve server. + */ + getVersion() { return this.details.version; } + + /** + * Returns the limit on the number of Sieve "redirect" actions a script can + * perform during a single evaluation. + * + * Note, this is different from the total number of "redirect" actions a + * script can contain. + * + * @returns {int} + * a non-negative number of redirects, or -1 for infinite redirects + */ + getMaxRedirects() { return this.details.maxredirects; } + + /** + * Returns a string array of URI schema parts for supported notification + * methods. This capability is be specified, if the Sieve implementation + * supports the "enotify" extension. + * + * @returns {string[]} + * The schema parts as string array + */ + getNotify() { return this.details.notify; } + + /** + * Returns the language currently used for human readable error messages. + * If this capability is not returned, the "i-default" [RFC2277] language is + * assumed. + * + * Note that the current language might be per-user configurable (i.e. it + * might change after authentication) + * + * @returns {string} + * a [RFC4646] conform language tag as string + */ + getLanguage() { return this.details.language; } + + /** + * Returns a list with sieve commands which are supported by this implementation + * and are not part of the absolute minimal ManageSieve support. + * + * The server advertises such additional commands either by explicitly + * naming the command or by using the compatibility level capability. + * + * Examples are RENAME, NOOP and CHECKSCRIPT. + * + * @returns {object} + * an associative array containing additional sieve commands + */ + getCompatibility() { return this.details.compatibility; } + + /** + * Gets the name of the logged in user. + * + * Note: This value is only available after AUTHENTICATE command succeeds + * + * @returns {string} + * a String containing the username + */ + getOwner() { return this.details.owner; } +} + + + +/** + * Parses list script response. + */ +class SieveListScriptsResponse extends SieveSimpleResponse { + + /** + * @inheritdoc + */ + parse(parser) { + // sieve-name = string + // string = quoted / literal + // (sieve-name [SP "ACTIVE"] CRLF) response-oknobye + + const scripts = []; + let i = -1; + + while (parser.isString()) { + i++; + + scripts[i] = {}; + scripts[i].script = parser.extractString(); + + if (parser.isLineBreak()) { + scripts[i].active = false; + parser.extractLineBreak(); + + continue; + } + + parser.extractSpace(); + + if (parser.extractToken([CHAR_CR]).toUpperCase() !== "ACTIVE") + throw new Error("Error \"ACTIVE\" expected"); + + scripts[i].active = true; + parser.extractLineBreak(); + + } + + this.scripts = scripts; + + return super.parse(parser); + } + + + /** + * An array of objects. Each object represents a script and + * has at least a property named script which contains the + * script name and a property named active which is either + * true or false. + * + * @returns {object[]} + * an array of objects with the name and activation state for each script + */ + getScripts() { + return this.scripts; + } +} + +/** + * Parses a get script response which returns the content + * of a script + */ +class SieveGetScriptResponse extends SieveSimpleResponse { + + /** + * Parses a get script response. + * + * It is perfectly fine for the server to return an empty script. + * + * @param {string} name + * the script name, to simplify the handling as the server just returns the content. + */ + constructor(name) { + super(); + this.scriptName = name; + } + + /** + * @inheritdoc + */ + parse(parser) { + let body = ""; + // [(<"> *1024QUOTED-CHAR <">) / ("{" number "+}" CRLF *OCTET) CRLF] response-oknobye + if (parser.isString()) { + body = parser.extractString(); + parser.extractLineBreak(); + } + + this.scriptBody = body; + + return super.parse(parser); + } + + /** + * Contains the requested sieve script. + * Keep in mind scripts can't be locked, so several clients may manipulate + * a script at the same time. + * + * @returns {string} returns the requested script's content + */ + getScriptBody() { return this.scriptBody; } + + /** + * Returns the requested script's name, + * + * @returns {string} the script name. + */ + getScriptName() { return this.scriptName; } +} + + +/** + * In contrast to a simple sieve request most of the sasl + * requests are more complex and require multiple round trips + * to be completed. Which means the response has to track + * the state. + * + * This simple wrapper makes the response stateful. + */ +class SieveStateFullResponse extends SieveSimpleResponse { + + /** + * @inheritdoc + */ + constructor() { + super(); + this.state = 0; + } + + /** + * Gets the responses current state + * + * @returns {int} + * the current state as integer + */ + getState() { return this.state; } + +} + +const STATE_LOGIN_USERNAME = 0; +const STATE_LOGIN_PASSWORD = 1; +const STATE_LOGIN_VERIFICATION = 2; +const STATE_LOGIN_COMPLETED = 4; + +/** + * SASL Login responses consist of multiple responses and requests. + * + * This means you need to call the parse method unless you reach the desired + * state in the state engine or unless an exception is thrown. + * + * The state can be retrieved by calling getState. + */ +class SieveSaslLoginResponse extends SieveStateFullResponse { + + /** + * @inheritdoc + */ + parse(parser) { + + if ((this.state === STATE_LOGIN_USERNAME) && (parser.isString())) { + // String should be 'Username:' or something similar + parser.extractString(); + parser.extractLineBreak(); + + this.state = STATE_LOGIN_PASSWORD; + return this; + } + + if ((this.state === STATE_LOGIN_PASSWORD) && (parser.isString())) { + // String should be equivalent to 'Password:' + parser.extractString(); + parser.extractLineBreak(); + + this.state = STATE_LOGIN_VERIFICATION; + return this; + } + + if (this.state === STATE_LOGIN_VERIFICATION) { + // Should be either a NO, BYE or OK + this.state = STATE_LOGIN_COMPLETED; + + super.parse(parser); + return this; + } + + // is it an error message? + try { + super.parse(parser); + } + catch (ex) { + throw new Error(`Illegal State: ${this.state} / ${parser.getData(0)}\n${ex}`); + } + + this.state = STATE_LOGIN_COMPLETED; + return this; + } +} + +const SHA_STATE_FIRST_MESSAGE = 0; +const SHA_STATE_FINAL_MESSAGE = 1; +const SHA_STATE_SIEVE_RESPONSE = 2; +const SHA_STATE_COMPLETED = 4; + +const SHA_FIRST_TOKEN = 0; +const SHA_PREFIX_LENGTH = 2; + +/** + * Parses responses for SCRAM-SHA authentication. + * + * SCRAM is a secure client first authentication mechanism. The client + * challenges the server and deicides if the connection is trustworthy. + * + * This requires a way mor logic on the client than with simple authentication + * mechanisms. It also requires more communication, in total two roundtrips. + */ +class SieveSaslScramShaResponse extends SieveStateFullResponse { + + /** + * Extracts the reserved-mext token from the array of tokens. + * @param {string[]} tokens + * the first message response split into tokens. + * @returns {string} + * the optional reserved-mext token or an empty string. + */ + _extractReservedMext(tokens) { + const token = tokens[SHA_FIRST_TOKEN]; + + // Test for the reserved-mext token. If it is existent, we just skip it + if ((token.length <= SHA_PREFIX_LENGTH) || !token.startsWith("m=")) + return ""; + + tokens.shift(); + return token.substr(SHA_FIRST_TOKEN); + } + + /** + * Extracts the nonce from the first message. + * @param {string[]} tokens + * the first message response split into tokens. + * @returns {string} + * the nonce or an exception in case it could not be extracted. + */ + _extractNonce(tokens) { + const token = tokens[SHA_FIRST_TOKEN]; + + // Extract the nonce + if ((token.length <= SHA_PREFIX_LENGTH) || !token.startsWith("r=")) + throw new Error(`Nonce expected but got ${token}`); + + tokens.shift(); + + // remove the "r=" + return token.substr(SHA_PREFIX_LENGTH); + } + + /** + * Extracts the salt from the first message. + * @param {string[]} tokens + * the first message response split into tokens. + * @returns {string} + * the salt as base64 encoded string or an exception in case + * it could not be extracted + */ + _extractSalt(tokens) { + const token = tokens[SHA_FIRST_TOKEN]; + + if ((token.length <= SHA_PREFIX_LENGTH) || !token.startsWith("s=")) + throw new Error(`Salt expected but got ${token}`); + + tokens.shift(); + + // remove the "s="" + return token.substr(SHA_PREFIX_LENGTH); + } + + /** + * Extracts the iteration count from the first message. + * @param {string[]} tokens + * the first message response split into tokens. + * @returns {int} + * the iteration count as integer or an exception in case the + * iterations could not be extracted. + */ + _extractIterations(tokens) { + const token = tokens[SHA_FIRST_TOKEN]; + + if ((token.length <= SHA_PREFIX_LENGTH) || !token.startsWith("i=")) + throw new Error(`Iteration Count expected but got ${token}`); + + tokens.shift(); + + // Remove the prefix and convert to an integer + return Number.parseInt(token.substr(SHA_PREFIX_LENGTH), 10); + } + + /** + * Parses the server-first-message it is defined to be: + * [reserved-mext ","] nonce "," salt "," iteration-count ["," extensions] + * + * Where + * reserved-mext : "m=" 1*(value-char) + * nonce : "r=" c-nonce + * salt : "s=" base64(salt) + * iteration-count : "i=" posit-number + * + * Extensions are optional and for future use. + * Neither c-nonce nor salt can contain a "," character + * + * @param {SieveResponseParser} parser + * the response parser which contains the response to be parsed. + * + * + * @private + */ + _parseFirstMessage(parser) { + + this._serverFirstMessage = + (new SieveBase64Decoder(parser.extractString())).toUtf8(); + + const tokens = this._serverFirstMessage.split(','); + + this._extractReservedMext(tokens); + this._nonce = this._extractNonce(tokens); + + this._salt = (new SieveBase64Decoder(this._extractSalt(tokens))).toArray(); + this._iter = this._extractIterations(tokens); + } + + /** + * Parses the server-final-message. + * + * It is defined to be: + * (server-error / verifier) ["," extensions] + * + * Where + * server-error : "e=" server-error-value + * verifier : "v=" base64(ServerSignature) + * + * Extensions are optional and for future use. + * As suggested by the RFC they will be ignored + * + * @param {SieveResponseParser} parser + * the parser which should be to process the message. + * @param {string} [data] + * optional, the server's final message. It omitted it + * will be parsed from the response. + * + * + * @private + */ + async _parseFinalMessage(parser, data) { + + if (typeof (data) === "undefined" || data === null) + data = parser.extractString(); + + // server-final-message = (server-error / verifier) ["," extensions] + data = (new SieveBase64Decoder(data)).toUtf8(); + + const token = data.split(",")[SHA_FIRST_TOKEN]; + + if (token.length <= SHA_PREFIX_LENGTH) + throw new Error(`Response expected but got: ${data}`); + + // server-error = "e=" + if (token.startsWith("e=")) { + this._serverError = token.substr(SHA_PREFIX_LENGTH); + return; + } + + // verifier = "v=" base64 + if (token.startsWith("v=")) { + this._verifier = (new SieveBase64Decoder( + token.substr(SHA_PREFIX_LENGTH))).toArray(); + return; + } + + throw new Error("Invalid Final message"); + } + + /** + * @inheritdoc + */ + async parse(parser) { + + if ((this.state === SHA_STATE_FIRST_MESSAGE) && (parser.isString())) { + await this._parseFirstMessage(parser); + parser.extractLineBreak(); + + this.state = SHA_STATE_FINAL_MESSAGE; + + return this; + } + + + // There are two valid responses... + // ... either the Server sends us an explicit final message: + // + // S: cnNwYXV0aD1lYTQwZjYwMzM1YzQyN2I1NTI3Yjg0ZGJhYmNkZmZmZA== + // C: "" + // S: OK + + if ((this.state === SHA_STATE_FINAL_MESSAGE) && (parser.isString())) { + + await this._parseFinalMessage(parser); + parser.extractLineBreak(); + + this.state = SHA_STATE_SIEVE_RESPONSE; + + return this; + } + + // Or the response is wrapped into the ResponseCode in order to save... + // ... roundtrip time so we end up with the following + // + // S: OK (SASL "cnNwYXV0aD1lYTQwZjYwMzM1YzQyN2I1NTI3Yjg0ZGJhYmNkZmZmZA==") + super.parse(parser); + + if (this.hasError()) + return this; + + if (this.state === SHA_STATE_SIEVE_RESPONSE) { + this.state = SHA_STATE_COMPLETED; + return this; + } + + if (this.state === SHA_STATE_FINAL_MESSAGE) { + await this._parseFinalMessage(parser, this.getResponseCode().getSasl()); + this.state = SHA_STATE_COMPLETED; + return this; + } + + return this; + } + + /** + * The salt is transferred with the first message and used to + * randomize the SHA request + * + * @returns {string} + * the salt + */ + getSalt() { + if (this.state < SHA_STATE_FINAL_MESSAGE) + throw new Error("Illegal State, request not completed"); + + return this._salt; + } + + /** + * Returns the number of iterations required by the server. + * It is only available after the final message was received. + * + * @returns {int} + * the number of iterations. + */ + getIterationCounter() { + if (this.state < SHA_STATE_FINAL_MESSAGE) + throw new Error("Illegal State, request not completed"); + + return this._iter; + } + + /** + * Returns the nonce. Please note this it is only available after the + * final message was received + * + * @returns {string} + * the nonce received from the server on an exception in case it is unknown. + */ + getNonce() { + if (this.state < SHA_STATE_FINAL_MESSAGE) + throw new Error("Illegal State, request not completed"); + + return this._nonce; + } + + /** + * Returns the servers first message. Pleas not is tis only available when the + * final message was received. + * + * @returns {string} + * the server's first message or an exception in case it is unknown. + */ + getServerFirstMessage() { + if (this.state < SHA_STATE_FINAL_MESSAGE) + throw new Error("Illegal State, request not completed"); + + return this._serverFirstMessage; + } + + /** + * Returns the error message returned by the server. + * + * @returns {string} + * the error message; + */ + getServerError() { + if (this.state < SHA_STATE_SIEVE_RESPONSE) + throw new Error("Illegal State, request not completed"); + + return this._serverError; + } + + /** + * The server's signature which needs to be verified. + * + * @returns {string} + * the server's signature + */ + getVerifier() { + if (this.state < SHA_STATE_SIEVE_RESPONSE) + throw new Error("Illegal State, request not completed"); + + return this._verifier; + } +} + +export { + SieveSimpleResponse, + SieveCapabilitiesResponse, + SieveListScriptsResponse, + SieveSaslLoginResponse, + SieveGetScriptResponse, + SieveSaslScramShaResponse +}; diff --git a/src/common/libManageSieve/SieveResponseCodes.js b/src/common/libManageSieve/SieveResponseCodes.js deleted file mode 100755 index 13dc1f1d..00000000 --- a/src/common/libManageSieve/SieveResponseCodes.js +++ /dev/null @@ -1,149 +0,0 @@ -/* - * The contents of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via email - * from the author. Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - // Enable Strict Mode - "use strict"; - - const RESPONSE_CODE_NAME = 0; - const RESPONSE_CODE_EXTENSION = 1; - - const NOT_FOUND = -1; - - /** - * The response codes is a string with optional additional arguments - */ - class SieveResponseCode { - - /** - * Creates a new instance. - * - * @param {string[]} code - * the response code including the additional arguments returned by the server. - */ - constructor(code) { - this.code = code; - } - - /** - * Response codes should not encapsulated in quotes according to the RFC. - * Never the less Cyrus Servers sometimes do encapsulate the response codes. - * - * This method is aware of this behaviour, and should be always when comparing - * ResponseCodes - * - * @param {string} code - * the response code which should be tested for equality - * @returns {boolean} - * true in case the response code is equal otherwise false. - */ - equalsCode(code) { - if ((!this.code) || (!this.code.length)) - return false; - - if (this.code[RESPONSE_CODE_NAME].toUpperCase() !== code.toUpperCase()) - return false; - - return true; - } - } - - - /** - * The server for sasl request always the response code "SASL" - * followed by additional information. - */ - class SieveResponseCodeSasl extends SieveResponseCode { - - /** - * @inheritdoc - */ - constructor(code) { - super(code); - - if (this.code[RESPONSE_CODE_NAME].toUpperCase() !== "SASL") - throw new Error("Malformed SASL Response Code"); - } - - /** - * Gets the sasl response code. - * - * @returns {string} - * Returns the sasl response - */ - getSasl() { - return this.code[RESPONSE_CODE_EXTENSION]; - } - } - - /** - * In case of a referral a special response code is returned. - * It contains the address to which the server refers us. - */ - class SieveResponseCodeReferral extends SieveResponseCode { - - /** - * @inheritdoc - */ - constructor(code) { - super(code); - - if (this.code[RESPONSE_CODE_NAME].toUpperCase() !== "REFERRAL") - throw new Error("Malformed REFERRAL Response Code"); - - // We should have received something similar to - // REFERRAL "sieve://c3.mail.example.com" - - // the quoted text contains the authority - // authority = [ userinfo "@" ] host [ ":" port ] - const uri = this.code[RESPONSE_CODE_EXTENSION]; - - // remove the sieve:// scheme - this.hostname = uri.slice("sieve://".length); - - // cleanup any script urls. - if (this.hostname.indexOf("/") >= 0) - this.hostname = this.hostname.slice(0, this.hostname.indexOf("/")); - - if (this.hostname.indexOf(":") === NOT_FOUND) - return; - - // extract the port - this.port = this.hostname.slice(this.hostname.indexOf(":") + ":".length); - this.hostname = this.hostname.slice(0, this.hostname.indexOf(":")); - } - - /** - * Returns the hostname of the referred server. - * - * @returns {string} - * the hostname as string. - */ - getHostname() { - return this.hostname; - } - - /** - * Returns the port of the referred server. If the server did not specify - * any Port null is returned. - * - * @returns {int} - * the port number or null - */ - getPort() { - return this.port; - } - } - - exports.SieveResponseCode = SieveResponseCode; - exports.SieveResponseCodeSasl = SieveResponseCodeSasl; - exports.SieveResponseCodeReferral = SieveResponseCodeReferral; - -})(module.exports || this); diff --git a/src/common/libManageSieve/SieveResponseCodes.mjs b/src/common/libManageSieve/SieveResponseCodes.mjs new file mode 100644 index 00000000..8bd276af --- /dev/null +++ b/src/common/libManageSieve/SieveResponseCodes.mjs @@ -0,0 +1,142 @@ +/* + * The contents of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via email + * from the author. Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +const RESPONSE_CODE_NAME = 0; +const RESPONSE_CODE_EXTENSION = 1; + +/** + * The response codes is a string with optional additional arguments + */ +class SieveResponseCode { + + /** + * Creates a new instance. + * + * @param {string[]} code + * the response code including the additional arguments returned by the server. + */ + constructor(code) { + this.code = code; + } + + /** + * Response codes should not encapsulated in quotes according to the RFC. + * Never the less Cyrus Servers sometimes do encapsulate the response codes. + * + * This method is aware of this behaviour, and should be always when comparing + * ResponseCodes + * + * @param {string} code + * the response code which should be tested for equality + * @returns {boolean} + * true in case the response code is equal otherwise false. + */ + equalsCode(code) { + if ((!this.code) || (!this.code.length)) + return false; + + if (this.code[RESPONSE_CODE_NAME].toUpperCase() !== code.toUpperCase()) + return false; + + return true; + } +} + + +/** + * The server for sasl request always the response code "SASL" + * followed by additional information. + */ +class SieveResponseCodeSasl extends SieveResponseCode { + + /** + * @inheritdoc + */ + constructor(code) { + super(code); + + if (this.code[RESPONSE_CODE_NAME].toUpperCase() !== "SASL") + throw new Error("Malformed SASL Response Code"); + } + + /** + * Gets the sasl response code. + * + * @returns {string} + * Returns the sasl response + */ + getSasl() { + return this.code[RESPONSE_CODE_EXTENSION]; + } +} + +/** + * In case of a referral a special response code is returned. + * It contains the address to which the server refers us. + */ +class SieveResponseCodeReferral extends SieveResponseCode { + + /** + * @inheritdoc + */ + constructor(code) { + super(code); + + if (this.code[RESPONSE_CODE_NAME].toUpperCase() !== "REFERRAL") + throw new Error("Malformed REFERRAL Response Code"); + + // We should have received something similar to + // REFERRAL "sieve://c3.mail.example.com" + + // the quoted text contains the authority + // authority = [ userinfo "@" ] host [ ":" port ] + const uri = this.code[RESPONSE_CODE_EXTENSION]; + + // remove the sieve:// scheme + this.hostname = uri.slice("sieve://".length); + + // cleanup any script urls. + if (this.hostname.includes("/")) + this.hostname = this.hostname.slice(0, this.hostname.indexOf("/")); + + if (!this.hostname.includes(":")) + return; + + // extract the port + this.port = this.hostname.slice(this.hostname.indexOf(":") + ":".length); + this.hostname = this.hostname.slice(0, this.hostname.indexOf(":")); + } + + /** + * Returns the hostname of the referred server. + * + * @returns {string} + * the hostname as string. + */ + getHostname() { + return this.hostname; + } + + /** + * Returns the port of the referred server. If the server did not specify + * any Port null is returned. + * + * @returns {int} + * the port number or null + */ + getPort() { + return this.port; + } +} + +export { + SieveResponseCode, + SieveResponseCodeSasl, + SieveResponseCodeReferral +}; diff --git a/src/common/libManageSieve/SieveResponseParser.mjs b/src/common/libManageSieve/SieveResponseParser.mjs new file mode 100644 index 00000000..66e0a4c3 --- /dev/null +++ b/src/common/libManageSieve/SieveResponseParser.mjs @@ -0,0 +1,425 @@ +/* + * The contents of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via email + * from the author. Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + + +const CHAR_LF = 10; +const CHAR_CR = 13; +const CHAR_SPACE = 32; +const CHAR_QUOTE = 34; +const CHAR_BACKSLASH = 92; +const CHAR_LEFT_BRACES = 123; +const CHAR_RIGHT_BRACES = 125; + +const NOT_FOUND = -1; +const CHAR_LEN = 1; + +/** + * The manage sieve protocol syntax uses a fixed grammar which is based on atomic tokens. + * This class offers an interface to test for and extract these predefined tokens. It supports + * Strings (Quoted and Literal), White Space (Line Break, Space ...) as well as arbitrary tokens. + * + * The parser does not change or alter the byte array's content. So extracting data does not shrink + * the array free any bytes. This parser is just some kind of a view to this array. + * + * Tokens are automatically converted from UTF-8 encoded byte arrays to JavaScript Unicode Strings + * during extraction. + */ +class SieveResponseParser { + + /** + * Expects as input a byte array using UTF-8 encoding. It's because the manage sieve + * protocol is defined to uses UTF-8 encoding and Mozilla sockets return byte based incoming messages streams. + * @param {byte[]} data + * the response which should be parsed as a byte array encoded in UTF-8 + */ + constructor(data) { + if ((typeof (data) === 'undefined') || (data === null)) + throw new Error("Error Parsing Response...\nData is null"); + + this.pos = 0; + this.data = data; + } + + /** + * Extracts the given number of bytes from the buffer. + * + * @param {int} size + * The number of bytes as integer which should be extracted + * + */ + extract(size) { + this.pos += size; + } + + /** + * Tests if the array starts with a line break (#13#10) + * + * @returns {boolean} + * true if the buffer with a line break, otherwise false + */ + isLineBreak() { + // Are we out of bounds? + if (this.data.length < this.pos + CHAR_LEN) + return false; + + // Test for a line break #13#10 + if (this.data[this.pos] !== CHAR_CR) + return false; + + if (this.data[this.pos + CHAR_LEN] !== CHAR_LF) + return false; + + return true; + } + + /** + * Extracts a line break (#13#10) for the buffer + * + * If it does not start with a line break an exception is thrown. + * + * @returns {SieveAbstractResponseParser} + * a self reference + */ + extractLineBreak() { + if (this.isLineBreak() === false) + throw new Error(`Line break expected but found:\n${this.getData()}`); + + this.pos += "\r\n".length; + + return this; + } + + /** + * Test if the buffer starts with a space character (#32) + * @returns {boolean} + * true if buffer starts with a space character, otherwise false + */ + isSpace() { + if (this.data[this.pos] === CHAR_SPACE) + return true; + + return false; + } + + /** + * Extracts a space character (#32) form the buffer + * + * If it does not start with a space character an exception is thrown. + * + */ + extractSpace() { + if (this.isSpace() === false) + throw new Error(`Space expected but found:\n${this.getData()}`); + + this.pos++; + } + + /** + * Tests if the current buffer position is a literal string. + * Literals strings are defined as: + * + * literal = "{" number "+}" CRLF *OCTET + * + * @returns {boolean} + * true in case it is a literal otherwise false. + */ + isLiteral() { + if (this.data[this.pos] === CHAR_LEFT_BRACES) + return true; + + return false; + } + + /** + * Extracts a literal string from the current position. + * Literals strings are defined as: + * + * literal = "{" number "+}" CRLF *OCTET + * + * Please not it is perfectly fine to have a literal with a zero byte length. + * + * @returns {string} + * the string or an exception in case the literal could not be extracted. + */ + extractLiteral() { + if (this.isLiteral() === false) + throw new Error(`Literal Expected but found\n ${this.getData()}`); + + // remove the "{" + this.pos++; + + // some sieve implementations are broken, this means .... + // ... we can get "{4+}\r\n1234" or "{4}\r\n1234" + + const nextBracket = this.indexOf(CHAR_RIGHT_BRACES); + if (nextBracket === NOT_FOUND) + throw new Error(`Error unbalanced parentheses "{" in \n ${this.getData()}`); + + // extract the size, and ignore "+" + const size = Number.parseInt(this.getData(this.pos, nextBracket).replace(/\+/, ""), 10); + + this.pos = nextBracket + CHAR_LEN; + + this.extractLineBreak(); + + // extract the literal... + const literal = this.getData(this.pos, this.pos + size); + this.pos += size; + + return literal; + } + + /** + * Searches the buffer for a character. + * + * @param {byte} character + * the character which should be found + * @param {int} [offset] + * an absolute offset, from which to start searching + * @returns {int} character + * the characters absolute position within the buffer otherwise -1 if not found + */ + indexOf(character, offset) { + if (typeof (offset) === "undefined") + offset = this.pos; + + for (let i = offset; i < this.data.length; i++) + if (this.data[i] === character) + return i; + + return NOT_FOUND; + } + + /** + * Test if the buffer starts with a quote character (#34) + * @returns {boolean} + * true if buffer starts with a quote character, otherwise false + */ + isQuoted() { + if (this.data[this.pos] === CHAR_QUOTE) + return true; + + return false; + } + + /** + * Extracts a quoted string form the buffer. It is aware of escape sequences. + * + * If it does not start with a valid string an exception is thrown. + * + * @returns {string} + * the quoted string extracted, it is guaranteed to be free of escape sequences + */ + extractQuoted() { + if (this.isQuoted() === false) + throw new Error(`Quoted string expected but found\n${this.getData()}`); + + // now search for the end. But we need to be aware of escape sequences. + let nextQuote = this.pos + CHAR_LEN; + + while (this.data[nextQuote] !== CHAR_QUOTE) { + + // Quoted stings can not contain line breaks... + if (this.data[nextQuote] === CHAR_LF) + throw new Error("Line break (LF) in Quoted String detected"); + + if (this.data[nextQuote] === CHAR_CR) + throw new Error("Line break (CR) in Quoted String detected"); + + // is it an escape sequence? + if (this.data[nextQuote] === CHAR_BACKSLASH) { + // Yes, it's a backslash so get the next char... + nextQuote++; + + // ... only \\ and \" are valid escape sequences + if ((this.data[nextQuote] !== CHAR_BACKSLASH) && (this.data[nextQuote] !== CHAR_QUOTE)) + throw new Error("Invalid Escape Sequence"); + } + + // move to the next character + nextQuote++; + + if (this.nextQuote >= this.data.length) + throw new Error("Unterminated Quoted string"); + } + + let quoted = this.getData(this.pos + CHAR_LEN, nextQuote); + + this.pos = nextQuote + CHAR_LEN; + + // Cleanup escape sequences + quoted = quoted.replace(/\\"/g, '"'); + quoted = quoted.replace(/\\\\/g, '\\'); + + return quoted; + } + + /** + * Tests if the a quoted or literal string starts at the current position. + * + * @returns {boolean} + * true if a strings starts, otherwise false + */ + isString() { + if (this.isQuoted()) + return true; + + if (this.isLiteral()) + return true; + + return false; + } + + /** + * Extracts a quoted or literal string from the current position + * + * @returns {string} + * the quote or literal string or an exception in case no string could be extracted. + */ + extractString() { + if (this.isQuoted()) + return this.extractQuoted(); + if (this.isLiteral()) + return this.extractLiteral(); + + throw new Error(`String expected but found\n${this.getData()}`); + } + + /** + * Extracts a token form a response. The token is being delimited by any + * separator. The extracted token does not include the separator. + * + * Throws an exception if none of the separators is found. + * + * @param {byte[]} separators + * an array containing possible token separators. The first match always wins. + * @returns {string} + * the extracted token. + */ + extractToken(separators) { + // Search for the separators, the one with the lowest index which is not... + // ... equal to -1 wins. The -2 indicates not initialized... + let index = NOT_FOUND; + + for (let i = 0; i < separators.length; i++) { + const idx = this.indexOf(separators[i], this.pos); + + if (idx === NOT_FOUND) + continue; + + if (index === NOT_FOUND) + index = idx; + else + index = Math.min(index, idx); + } + + if (index === NOT_FOUND) + throw new Error(`Delimiter >>${separators}<< not found in: ${this.getData()}`); + + const token = this.getData(this.pos, index); + this.pos = index; + + return token; + } + + /** + * Tests if the buffer starts with the specified bytes. + * + * As the buffer is encoded in UTF-8, the specified bytes have to be + * encoded in UTF-8, otherwise the result is unpredictable. + * + * @param {Byte[]} bytes + * the bytes to compare as byte array encoded in UTF-8 + * + * @returns {boolean} + * true if bytes match the beginning of the buffer, otherwise false + */ + startsWith(bytes) { + if (!bytes.length) + return false; + + for (let i = 0; i < bytes.length; i++) { + let result = false; + + for (let ii = 0; ii < bytes[i].length; ii++) + if (bytes[i][ii] === this.data[this.pos + i]) + result = true; + + if (result === false) + return false; + } + + return true; + } + + /** + * Returns a copy of the current buffer. + * + * @returns {byte[]} + * an a copy of the array's current view. It is encoded in UTF-8 + */ + getByteArray() { + return this.data.slice(this.pos, this.data.length); + } + + /** + * Returns a copy of the response parsers buffer as JavaScript Unicode string. + * + * Manage Sieve encodes literals in UTF-8 while network sockets are usually + * binary. So we can't use java scripts build in string functions as they expect + * pure unicode. + * + * @param {int} [startIndex] + * Optional zero-based index at which to begin. + * @param {int} [endIndex] + * Optional Zero-based index at which to end. + * @returns {string} the copy buffers content + */ + getData(startIndex, endIndex) { + + if (typeof (endIndex) === "undefined" || endIndex === null) + endIndex = this.data.length; + + if (typeof (startIndex) === "undefined" || startIndex === null) + startIndex = this.pos; + + const byteArray = this.data.slice(startIndex, endIndex); + + return (new TextDecoder("UTF-8")).decode(new Uint8Array(byteArray)); + } + + + /** + * Check if the buffer is empty. This means the buffer does not contain any + * extractable bytes or tokens. + * + * @returns {boolean} + * true if the buffer is empty, otherwise false + */ + isEmpty() { + if (this.data.length >= this.pos) + return true; + + return false; + } + + + /** + * Returns the read pointes current position. + * Can be used to resync the parser with a buffer. + * + * @returns {int} + * the current read pointer offset relative to the start in bytes. + */ + getPosition() { + return this.pos; + } +} + +export { SieveResponseParser }; diff --git a/src/common/libManageSieve/doc/HMAC/rfc2104.txt b/src/common/libManageSieve/doc/HMAC/rfc2104.txt new file mode 100644 index 00000000..1fb8fe11 --- /dev/null +++ b/src/common/libManageSieve/doc/HMAC/rfc2104.txt @@ -0,0 +1,619 @@ + + + + + + +Network Working Group H. Krawczyk +Request for Comments: 2104 IBM +Category: Informational M. Bellare + UCSD + R. Canetti + IBM + February 1997 + + + HMAC: Keyed-Hashing for Message Authentication + +Status of This Memo + + This memo provides information for the Internet community. This memo + does not specify an Internet standard of any kind. Distribution of + this memo is unlimited. + +Abstract + + This document describes HMAC, a mechanism for message authentication + using cryptographic hash functions. HMAC can be used with any + iterative cryptographic hash function, e.g., MD5, SHA-1, in + combination with a secret shared key. The cryptographic strength of + HMAC depends on the properties of the underlying hash function. + +1. Introduction + + Providing a way to check the integrity of information transmitted + over or stored in an unreliable medium is a prime necessity in the + world of open computing and communications. Mechanisms that provide + such integrity check based on a secret key are usually called + "message authentication codes" (MAC). Typically, message + authentication codes are used between two parties that share a secret + key in order to validate information transmitted between these + parties. In this document we present such a MAC mechanism based on + cryptographic hash functions. This mechanism, called HMAC, is based + on work by the authors [BCK1] where the construction is presented and + cryptographically analyzed. We refer to that work for the details on + the rationale and security analysis of HMAC, and its comparison to + other keyed-hash methods. + + + + + + + + + + + +Krawczyk, et. al. Informational [Page 1] + +RFC 2104 HMAC February 1997 + + + HMAC can be used in combination with any iterated cryptographic hash + function. MD5 and SHA-1 are examples of such hash functions. HMAC + also uses a secret key for calculation and verification of the + message authentication values. The main goals behind this + construction are + + * To use, without modifications, available hash functions. + In particular, hash functions that perform well in software, + and for which code is freely and widely available. + + * To preserve the original performance of the hash function without + incurring a significant degradation. + + * To use and handle keys in a simple way. + + * To have a well understood cryptographic analysis of the strength of + the authentication mechanism based on reasonable assumptions on the + underlying hash function. + + * To allow for easy replaceability of the underlying hash function in + case that faster or more secure hash functions are found or + required. + + This document specifies HMAC using a generic cryptographic hash + function (denoted by H). Specific instantiations of HMAC need to + define a particular hash function. Current candidates for such hash + functions include SHA-1 [SHA], MD5 [MD5], RIPEMD-128/160 [RIPEMD]. + These different realizations of HMAC will be denoted by HMAC-SHA1, + HMAC-MD5, HMAC-RIPEMD, etc. + + Note: To the date of writing of this document MD5 and SHA-1 are the + most widely used cryptographic hash functions. MD5 has been recently + shown to be vulnerable to collision search attacks [Dobb]. This + attack and other currently known weaknesses of MD5 do not compromise + the use of MD5 within HMAC as specified in this document (see + [Dobb]); however, SHA-1 appears to be a cryptographically stronger + function. To this date, MD5 can be considered for use in HMAC for + applications where the superior performance of MD5 is critical. In + any case, implementers and users need to be aware of possible + cryptanalytic developments regarding any of these cryptographic hash + functions, and the eventual need to replace the underlying hash + function. (See section 6 for more information on the security of + HMAC.) + + + + + + + + +Krawczyk, et. al. Informational [Page 2] + +RFC 2104 HMAC February 1997 + + +2. Definition of HMAC + + The definition of HMAC requires a cryptographic hash function, which + we denote by H, and a secret key K. We assume H to be a cryptographic + hash function where data is hashed by iterating a basic compression + function on blocks of data. We denote by B the byte-length of such + blocks (B=64 for all the above mentioned examples of hash functions), + and by L the byte-length of hash outputs (L=16 for MD5, L=20 for + SHA-1). The authentication key K can be of any length up to B, the + block length of the hash function. Applications that use keys longer + than B bytes will first hash the key using H and then use the + resultant L byte string as the actual key to HMAC. In any case the + minimal recommended length for K is L bytes (as the hash output + length). See section 3 for more information on keys. + + We define two fixed and different strings ipad and opad as follows + (the 'i' and 'o' are mnemonics for inner and outer): + + ipad = the byte 0x36 repeated B times + opad = the byte 0x5C repeated B times. + + To compute HMAC over the data `text' we perform + + H(K XOR opad, H(K XOR ipad, text)) + + Namely, + + (1) append zeros to the end of K to create a B byte string + (e.g., if K is of length 20 bytes and B=64, then K will be + appended with 44 zero bytes 0x00) + (2) XOR (bitwise exclusive-OR) the B byte string computed in step + (1) with ipad + (3) append the stream of data 'text' to the B byte string resulting + from step (2) + (4) apply H to the stream generated in step (3) + (5) XOR (bitwise exclusive-OR) the B byte string computed in + step (1) with opad + (6) append the H result from step (4) to the B byte string + resulting from step (5) + (7) apply H to the stream generated in step (6) and output + the result + + For illustration purposes, sample code based on MD5 is provided as an + appendix. + + + + + + + +Krawczyk, et. al. Informational [Page 3] + +RFC 2104 HMAC February 1997 + + +3. Keys + + The key for HMAC can be of any length (keys longer than B bytes are + first hashed using H). However, less than L bytes is strongly + discouraged as it would decrease the security strength of the + function. Keys longer than L bytes are acceptable but the extra + length would not significantly increase the function strength. (A + longer key may be advisable if the randomness of the key is + considered weak.) + + Keys need to be chosen at random (or using a cryptographically strong + pseudo-random generator seeded with a random seed), and periodically + refreshed. (Current attacks do not indicate a specific recommended + frequency for key changes as these attacks are practically + infeasible. However, periodic key refreshment is a fundamental + security practice that helps against potential weaknesses of the + function and keys, and limits the damage of an exposed key.) + +4. Implementation Note + + HMAC is defined in such a way that the underlying hash function H can + be used with no modification to its code. In particular, it uses the + function H with the pre-defined initial value IV (a fixed value + specified by each iterative hash function to initialize its + compression function). However, if desired, a performance + improvement can be achieved at the cost of (possibly) modifying the + code of H to support variable IVs. + + The idea is that the intermediate results of the compression function + on the B-byte blocks (K XOR ipad) and (K XOR opad) can be precomputed + only once at the time of generation of the key K, or before its first + use. These intermediate results are stored and then used to + initialize the IV of H each time that a message needs to be + authenticated. This method saves, for each authenticated message, + the application of the compression function of H on two B-byte blocks + (i.e., on (K XOR ipad) and (K XOR opad)). Such a savings may be + significant when authenticating short streams of data. We stress + that the stored intermediate values need to be treated and protected + the same as secret keys. + + Choosing to implement HMAC in the above way is a decision of the + local implementation and has no effect on inter-operability. + + + + + + + + + +Krawczyk, et. al. Informational [Page 4] + +RFC 2104 HMAC February 1997 + + +5. Truncated output + + A well-known practice with message authentication codes is to + truncate the output of the MAC and output only part of the bits + (e.g., [MM, ANSI]). Preneel and van Oorschot [PV] show some + analytical advantages of truncating the output of hash-based MAC + functions. The results in this area are not absolute as for the + overall security advantages of truncation. It has advantages (less + information on the hash result available to an attacker) and + disadvantages (less bits to predict for the attacker). Applications + of HMAC can choose to truncate the output of HMAC by outputting the t + leftmost bits of the HMAC computation for some parameter t (namely, + the computation is carried in the normal way as defined in section 2 + above but the end result is truncated to t bits). We recommend that + the output length t be not less than half the length of the hash + output (to match the birthday attack bound) and not less than 80 bits + (a suitable lower bound on the number of bits that need to be + predicted by an attacker). We propose denoting a realization of HMAC + that uses a hash function H with t bits of output as HMAC-H-t. For + example, HMAC-SHA1-80 denotes HMAC computed using the SHA-1 function + and with the output truncated to 80 bits. (If the parameter t is not + specified, e.g. HMAC-MD5, then it is assumed that all the bits of the + hash are output.) + +6. Security + + The security of the message authentication mechanism presented here + depends on cryptographic properties of the hash function H: the + resistance to collision finding (limited to the case where the + initial value is secret and random, and where the output of the + function is not explicitly available to the attacker), and the + message authentication property of the compression function of H when + applied to single blocks (in HMAC these blocks are partially unknown + to an attacker as they contain the result of the inner H computation + and, in particular, cannot be fully chosen by the attacker). + + These properties, and actually stronger ones, are commonly assumed + for hash functions of the kind used with HMAC. In particular, a hash + function for which the above properties do not hold would become + unsuitable for most (probably, all) cryptographic applications, + including alternative message authentication schemes based on such + functions. (For a complete analysis and rationale of the HMAC + function the reader is referred to [BCK1].) + + + + + + + + +Krawczyk, et. al. Informational [Page 5] + +RFC 2104 HMAC February 1997 + + + Given the limited confidence gained so far as for the cryptographic + strength of candidate hash functions, it is important to observe the + following two properties of the HMAC construction and its secure use + for message authentication: + + 1. The construction is independent of the details of the particular + hash function H in use and then the latter can be replaced by any + other secure (iterative) cryptographic hash function. + + 2. Message authentication, as opposed to encryption, has a + "transient" effect. A published breaking of a message authentication + scheme would lead to the replacement of that scheme, but would have + no adversarial effect on information authenticated in the past. This + is in sharp contrast with encryption, where information encrypted + today may suffer from exposure in the future if, and when, the + encryption algorithm is broken. + + The strongest attack known against HMAC is based on the frequency of + collisions for the hash function H ("birthday attack") [PV,BCK2], and + is totally impractical for minimally reasonable hash functions. + + As an example, if we consider a hash function like MD5 where the + output length equals L=16 bytes (128 bits) the attacker needs to + acquire the correct message authentication tags computed (with the + _same_ secret key K!) on about 2**64 known plaintexts. This would + require the processing of at least 2**64 blocks under H, an + impossible task in any realistic scenario (for a block length of 64 + bytes this would take 250,000 years in a continuous 1Gbps link, and + without changing the secret key K during all this time). This attack + could become realistic only if serious flaws in the collision + behavior of the function H are discovered (e.g. collisions found + after 2**30 messages). Such a discovery would determine the immediate + replacement of the function H (the effects of such failure would be + far more severe for the traditional uses of H in the context of + digital signatures, public key certificates, etc.). + + Note: this attack needs to be strongly contrasted with regular + collision attacks on cryptographic hash functions where no secret key + is involved and where 2**64 off-line parallelizable (!) operations + suffice to find collisions. The latter attack is approaching + feasibility [VW] while the birthday attack on HMAC is totally + impractical. (In the above examples, if one uses a hash function + with, say, 160 bit of output then 2**64 should be replaced by 2**80.) + + + + + + + + +Krawczyk, et. al. Informational [Page 6] + +RFC 2104 HMAC February 1997 + + + A correct implementation of the above construction, the choice of + random (or cryptographically pseudorandom) keys, a secure key + exchange mechanism, frequent key refreshments, and good secrecy + protection of keys are all essential ingredients for the security of + the integrity verification mechanism provided by HMAC. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Krawczyk, et. al. Informational [Page 7] + +RFC 2104 HMAC February 1997 + + +Appendix -- Sample Code + + For the sake of illustration we provide the following sample code for + the implementation of HMAC-MD5 as well as some corresponding test + vectors (the code is based on MD5 code as described in [MD5]). + +/* +** Function: hmac_md5 +*/ + +void +hmac_md5(text, text_len, key, key_len, digest) +unsigned char* text; /* pointer to data stream */ +int text_len; /* length of data stream */ +unsigned char* key; /* pointer to authentication key */ +int key_len; /* length of authentication key */ +caddr_t digest; /* caller digest to be filled in */ + +{ + MD5_CTX context; + unsigned char k_ipad[65]; /* inner padding - + * key XORd with ipad + */ + unsigned char k_opad[65]; /* outer padding - + * key XORd with opad + */ + unsigned char tk[16]; + int i; + /* if key is longer than 64 bytes reset it to key=MD5(key) */ + if (key_len > 64) { + + MD5_CTX tctx; + + MD5Init(&tctx); + MD5Update(&tctx, key, key_len); + MD5Final(tk, &tctx); + + key = tk; + key_len = 16; + } + + /* + * the HMAC_MD5 transform looks like: + * + * MD5(K XOR opad, MD5(K XOR ipad, text)) + * + * where K is an n byte key + * ipad is the byte 0x36 repeated 64 times + + + +Krawczyk, et. al. Informational [Page 8] + +RFC 2104 HMAC February 1997 + + + * opad is the byte 0x5c repeated 64 times + * and text is the data being protected + */ + + /* start out by storing key in pads */ + bzero( k_ipad, sizeof k_ipad); + bzero( k_opad, sizeof k_opad); + bcopy( key, k_ipad, key_len); + bcopy( key, k_opad, key_len); + + /* XOR key with ipad and opad values */ + for (i=0; i<64; i++) { + k_ipad[i] ^= 0x36; + k_opad[i] ^= 0x5c; + } + /* + * perform inner MD5 + */ + MD5Init(&context); /* init context for 1st + * pass */ + MD5Update(&context, k_ipad, 64) /* start with inner pad */ + MD5Update(&context, text, text_len); /* then text of datagram */ + MD5Final(digest, &context); /* finish up 1st pass */ + /* + * perform outer MD5 + */ + MD5Init(&context); /* init context for 2nd + * pass */ + MD5Update(&context, k_opad, 64); /* start with outer pad */ + MD5Update(&context, digest, 16); /* then results of 1st + * hash */ + MD5Final(digest, &context); /* finish up 2nd pass */ +} + +Test Vectors (Trailing '\0' of a character string not included in test): + + key = 0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b + key_len = 16 bytes + data = "Hi There" + data_len = 8 bytes + digest = 0x9294727a3638bb1c13f48ef8158bfc9d + + key = "Jefe" + data = "what do ya want for nothing?" + data_len = 28 bytes + digest = 0x750c783e6ab0b503eaa86e310a5db738 + + key = 0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + + + +Krawczyk, et. al. Informational [Page 9] + +RFC 2104 HMAC February 1997 + + + key_len 16 bytes + data = 0xDDDDDDDDDDDDDDDDDDDD... + ..DDDDDDDDDDDDDDDDDDDD... + ..DDDDDDDDDDDDDDDDDDDD... + ..DDDDDDDDDDDDDDDDDDDD... + ..DDDDDDDDDDDDDDDDDDDD + data_len = 50 bytes + digest = 0x56be34521d144c88dbb8c733f0e8b3f6 + +Acknowledgments + + Pau-Chen Cheng, Jeff Kraemer, and Michael Oehler, have provided + useful comments on early drafts, and ran the first interoperability + tests of this specification. Jeff and Pau-Chen kindly provided the + sample code and test vectors that appear in the appendix. Burt + Kaliski, Bart Preneel, Matt Robshaw, Adi Shamir, and Paul van + Oorschot have provided useful comments and suggestions during the + investigation of the HMAC construction. + +References + + [ANSI] ANSI X9.9, "American National Standard for Financial + Institution Message Authentication (Wholesale)," American + Bankers Association, 1981. Revised 1986. + + [Atk] Atkinson, R., "IP Authentication Header", RFC 1826, August + 1995. + + [BCK1] M. Bellare, R. Canetti, and H. Krawczyk, + "Keyed Hash Functions and Message Authentication", + Proceedings of Crypto'96, LNCS 1109, pp. 1-15. + (http://www.research.ibm.com/security/keyed-md5.html) + + [BCK2] M. Bellare, R. Canetti, and H. Krawczyk, + "Pseudorandom Functions Revisited: The Cascade Construction", + Proceedings of FOCS'96. + + [Dobb] H. Dobbertin, "The Status of MD5 After a Recent Attack", + RSA Labs' CryptoBytes, Vol. 2 No. 2, Summer 1996. + http://www.rsa.com/rsalabs/pubs/cryptobytes.html + + [PV] B. Preneel and P. van Oorschot, "Building fast MACs from hash + functions", Advances in Cryptology -- CRYPTO'95 Proceedings, + Lecture Notes in Computer Science, Springer-Verlag Vol.963, + 1995, pp. 1-14. + + [MD5] Rivest, R., "The MD5 Message-Digest Algorithm", + RFC 1321, April 1992. + + + +Krawczyk, et. al. Informational [Page 10] + +RFC 2104 HMAC February 1997 + + + [MM] Meyer, S. and Matyas, S.M., Cryptography, New York Wiley, + 1982. + + [RIPEMD] H. Dobbertin, A. Bosselaers, and B. Preneel, "RIPEMD-160: A + strengthened version of RIPEMD", Fast Software Encryption, + LNCS Vol 1039, pp. 71-82. + ftp://ftp.esat.kuleuven.ac.be/pub/COSIC/bosselae/ripemd/. + + [SHA] NIST, FIPS PUB 180-1: Secure Hash Standard, April 1995. + + [Tsu] G. Tsudik, "Message authentication with one-way hash + functions", In Proceedings of Infocom'92, May 1992. + (Also in "Access Control and Policy Enforcement in + Internetworks", Ph.D. Dissertation, Computer Science + Department, University of Southern California, April 1991.) + + [VW] P. van Oorschot and M. Wiener, "Parallel Collision + Search with Applications to Hash Functions and Discrete + Logarithms", Proceedings of the 2nd ACM Conf. Computer and + Communications Security, Fairfax, VA, November 1994. + +Authors' Addresses + + Hugo Krawczyk + IBM T.J. Watson Research Center + P.O.Box 704 + Yorktown Heights, NY 10598 + + EMail: hugo@watson.ibm.com + + Mihir Bellare + Dept of Computer Science and Engineering + Mail Code 0114 + University of California at San Diego + 9500 Gilman Drive + La Jolla, CA 92093 + + EMail: mihir@cs.ucsd.edu + + Ran Canetti + IBM T.J. Watson Research Center + P.O.Box 704 + Yorktown Heights, NY 10598 + + EMail: canetti@watson.ibm.com + + + + + + +Krawczyk, et. al. Informational [Page 11] + diff --git a/src/common/libManageSieve/doc/HMAC/rfc4086.txt b/src/common/libManageSieve/doc/HMAC/rfc4086.txt new file mode 100644 index 00000000..91041845 --- /dev/null +++ b/src/common/libManageSieve/doc/HMAC/rfc4086.txt @@ -0,0 +1,2691 @@ + + + + + + +Network Working Group D. Eastlake, 3rd +Request for Comments: 4086 Motorola Laboratories +BCP: 106 J. Schiller +Obsoletes: 1750 MIT +Category: Best Current Practice S. Crocker + June 2005 + + Randomness Requirements for Security + +Status of This Memo + + This document specifies an Internet Best Current Practices for the + Internet Community, and requests discussion and suggestions for + improvements. Distribution of this memo is unlimited. + +Copyright Notice + + Copyright (C) The Internet Society (2005). + +Abstract + + Security systems are built on strong cryptographic algorithms that + foil pattern analysis attempts. However, the security of these + systems is dependent on generating secret quantities for passwords, + cryptographic keys, and similar quantities. The use of pseudo-random + processes to generate secret quantities can result in pseudo- + security. A sophisticated attacker may find it easier to reproduce + the environment that produced the secret quantities and to search the + resulting small set of possibilities than to locate the quantities in + the whole of the potential number space. + + Choosing random quantities to foil a resourceful and motivated + adversary is surprisingly difficult. This document points out many + pitfalls in using poor entropy sources or traditional pseudo-random + number generation techniques for generating such quantities. It + recommends the use of truly random hardware techniques and shows that + the existing hardware on many systems can be used for this purpose. + It provides suggestions to ameliorate the problem when a hardware + solution is not available, and it gives examples of how large such + quantities need to be for some applications. + + + + + + + + + + + +Eastlake, et al. Standards Track [Page 1] + +RFC 4086 Randomness Requirements for Security June 2005 + + +Table of Contents + + 1. Introduction and Overview .......................................3 + 2. General Requirements ............................................4 + 3. Entropy Sources .................................................7 + 3.1. Volume Required ............................................7 + 3.2. Existing Hardware Can Be Used For Randomness ...............8 + 3.2.1. Using Existing Sound/Video Input ....................8 + 3.2.2. Using Existing Disk Drives ..........................8 + 3.3. Ring Oscillator Sources ....................................9 + 3.4. Problems with Clocks and Serial Numbers ...................10 + 3.5. Timing and Value of External Events .......................11 + 3.6. Non-hardware Sources of Randomness ........................12 + 4. De-skewing .....................................................12 + 4.1. Using Stream Parity to De-Skew ............................13 + 4.2. Using Transition Mappings to De-Skew ......................14 + 4.3. Using FFT to De-Skew ......................................15 + 4.4. Using Compression to De-Skew ..............................15 + 5. Mixing .........................................................16 + 5.1. A Trivial Mixing Function .................................17 + 5.2. Stronger Mixing Functions .................................18 + 5.3. Using S-Boxes for Mixing ..................................19 + 5.4. Diffie-Hellman as a Mixing Function .......................19 + 5.5. Using a Mixing Function to Stretch Random Bits ............20 + 5.6. Other Factors in Choosing a Mixing Function ...............20 + 6. Pseudo-random Number Generators ................................21 + 6.1. Some Bad Ideas ............................................21 + 6.1.1. The Fallacy of Complex Manipulation ................21 + 6.1.2. The Fallacy of Selection from a Large Database .....22 + 6.1.3. Traditional Pseudo-random Sequences ................23 + 6.2. Cryptographically Strong Sequences ........................24 + 6.2.1. OFB and CTR Sequences ..............................25 + 6.2.2. The Blum Blum Shub Sequence Generator ..............26 + 6.3. Entropy Pool Techniques ...................................27 + 7. Randomness Generation Examples and Standards ...................28 + 7.1. Complete Randomness Generators ............................28 + 7.1.1. US DoD Recommendations for Password Generation .....28 + 7.1.2. The /dev/random Device .............................29 + 7.1.3. Windows CryptGenRandom .............................30 + 7.2. Generators Assuming a Source of Entropy ...................31 + 7.2.1. X9.82 Pseudo-Random Number Generation ..............31 + 7.2.2. X9.17 Key Generation ...............................33 + 7.2.3. DSS Pseudo-random Number Generation ................34 + 8. Examples of Randomness Required ................................34 + 8.1. Password Generation .......................................35 + 8.2. A Very High Security Cryptographic Key ....................36 + 9. Conclusion .....................................................38 + 10. Security Considerations ........................................38 + + + +Eastlake, et al. Standards Track [Page 2] + +RFC 4086 Randomness Requirements for Security June 2005 + + + 11. Acknowledgments ................................................39 + Appendix A: Changes from RFC 1750 ..................................40 + Informative References .............................................41 + +1. Introduction and Overview + + Software cryptography is coming into wider use, although there is a + long way to go until it becomes pervasive. Systems such as SSH, + IPSEC, TLS, S/MIME, PGP, DNSSEC, and Kerberos are maturing and + becoming a part of the network landscape [SSH] [IPSEC] [TLS] [S/MIME] + [MAIL_PGP*] [DNSSEC*]. For comparison, when the previous version of + this document [RFC1750] was issued in 1994, the only Internet + cryptographic security specification in the IETF was the Privacy + Enhanced Mail protocol [MAIL_PEM*]. + + These systems provide substantial protection against snooping and + spoofing. However, there is a potential flaw. At the heart of all + cryptographic systems is the generation of secret, unguessable (i.e., + random) numbers. + + The lack of generally available facilities for generating such random + numbers (that is, the lack of general availability of truly + unpredictable sources) forms an open wound in the design of + cryptographic software. For the software developer who wants to + build a key or password generation procedure that runs on a wide + range of hardware, this is a very real problem. + + Note that the requirement is for data that an adversary has a very + low probability of guessing or determining. This can easily fail if + pseudo-random data is used that meets only traditional statistical + tests for randomness, or that is based on limited-range sources such + as clocks. Sometimes such pseudo-random quantities can be guessed by + an adversary searching through an embarrassingly small space of + possibilities. + + This Best Current Practice document describes techniques for + producing random quantities that will be resistant to attack. It + recommends that future systems include hardware random number + generation or provide access to existing hardware that can be used + for this purpose. It suggests methods for use if such hardware is + not available, and it gives some estimates of the number of random + bits required for sample applications. + + + + + + + + + +Eastlake, et al. Standards Track [Page 3] + +RFC 4086 Randomness Requirements for Security June 2005 + + +2. General Requirements + + Today, a commonly encountered randomness requirement is to pick a + user password, usually a simple character string. Obviously, a + password that can be guessed does not provide security. For re- + usable passwords, it is desirable that users be able to remember the + password. This may make it advisable to use pronounceable character + strings or phrases composed of ordinary words. But this affects only + the format of the password information, not the requirement that the + password be very hard to guess. + + Many other requirements come from the cryptographic arena. + Cryptographic techniques can be used to provide a variety of + services, including confidentiality and authentication. Such + services are based on quantities, traditionally called "keys", that + are unknown to and unguessable by an adversary. + + There are even TCP/IP protocol uses for randomness in picking initial + sequence numbers [RFC1948]. + + Generally speaking, the above examples also illustrate two different + types of random quantities that may be wanted. In the case of + human-usable passwords, the only important characteristic is that + they be unguessable. It is not important that they may be composed + of ASCII characters, so the top bit of every byte is zero, for + example. On the other hand, for fixed length keys and the like, one + normally wants quantities that appear to be truly random, that is, + quantities whose bits will pass statistical randomness tests. + + In some cases, such as the use of symmetric encryption with the one- + time pads or an algorithm like the US Advanced Encryption Standard + [AES], the parties who wish to communicate confidentially and/or with + authentication must all know the same secret key. In other cases, + where asymmetric or "public key" cryptographic techniques are used, + keys come in pairs. One key of the pair is private and must be kept + secret by one party; the other is public and can be published to the + world. It is computationally infeasible to determine the private key + from the public key, and knowledge of the public key is of no help to + an adversary [ASYMMETRIC]. See general references [SCHNEIER, + FERGUSON, KAUFMAN]. + + The frequency and volume of the requirement for random quantities + differs greatly for different cryptographic systems. With pure RSA, + random quantities are required only when a new key pair is generated; + thereafter, any number of messages can be signed without a further + need for randomness. The public key Digital Signature Algorithm + devised by the US National Institute of Standards and Technology + (NIST) requires good random numbers for each signature [DSS]. And + + + +Eastlake, et al. Standards Track [Page 4] + +RFC 4086 Randomness Requirements for Security June 2005 + + + encrypting with a one-time pad (in principle the strongest possible + encryption technique) requires randomness of equal volume to all the + messages to be processed. See general references [SCHNEIER, + FERGUSON, KAUFMAN]. + + In most of these cases, an adversary can try to determine the + "secret" key by trial and error. This is possible as long as the key + is enough smaller than the message that the correct key can be + uniquely identified. The probability of an adversary succeeding at + this must be made acceptably low, depending on the particular + application. The size of the space the adversary must search is + related to the amount of key "information" present, in an + information-theoretic sense [SHANNON]. This depends on the number of + different secret values possible and the probability of each value, + as follows: + + ----- + \ + Bits of information = \ - p * log ( p ) + / i 2 i + / + ----- + + where i counts from 1 to the number of possible secret values and p + sub i is the probability of the value numbered i. (Because p sub i + is less than one, the log will be negative, so each term in the sum + will be non-negative.) + + If there are 2^n different values of equal probability, then n bits + of information are present and an adversary would have to try, on the + average, half of the values, or 2^(n-1), before guessing the secret + quantity. If the probability of different values is unequal, then + there is less information present, and fewer guesses will, on + average, be required by an adversary. In particular, any values that + an adversary can know to be impossible or of low probability can be + initially ignored by the adversary, who will search through the more + probable values first. + + For example, consider a cryptographic system that uses 128-bit keys. + If these keys are derived using a fixed pseudo-random number + generator that is seeded with an 8-bit seed, then an adversary needs + to search through only 256 keys (by running the pseudo-random number + generator with every possible seed), not 2^128 keys as may at first + appear to be the case. Only 8 bits of "information" are in these + 128-bit keys. + + + + + + +Eastlake, et al. Standards Track [Page 5] + +RFC 4086 Randomness Requirements for Security June 2005 + + + While the above analysis is correct on average, it can be misleading + in some cases for cryptographic analysis where what is really + important is the work factor for an adversary. For example, assume + that there is a pseudo-random number generator generating 128-bit + keys, as in the previous paragraph, but that it generates zero half + of the time and a random selection from the remaining 2^128 - 1 + values the rest of the time. The Shannon equation above says that + there are 64 bits of information in one of these key values, but an + adversary, simply by trying the value zero, can break the security of + half of the uses, albeit a random half. Thus, for cryptographic + purposes, it is also useful to look at other measures, such as min- + entropy, defined as + + Min-entropy = - log ( maximum ( p ) ) + i + + where i is as above. Using this equation, we get 1 bit of min- + entropy for our new hypothetical distribution, as opposed to 64 bits + of classical Shannon entropy. + + A continuous spectrum of entropies, sometimes called Renyi entropy, + has been defined, specified by the parameter r. Here r = 1 is + Shannon entropy and r = infinity is min-entropy. When r = zero, it + is just log (n), where n is the number of non-zero probabilities. + Renyi entropy is a non-increasing function of r, so min-entropy is + always the most conservative measure of entropy and usually the best + to use for cryptographic evaluation [LUBY]. + + Statistically tested randomness in the traditional sense is NOT the + same as the unpredictability required for security use. + + For example, the use of a widely available constant sequence, such as + the random table from the CRC Standard Mathematical Tables, is very + weak against an adversary. An adversary who learns of or guesses it + can easily break all security, future and past, based on the sequence + [CRC]. As another example, using AES with a constant key to encrypt + successive integers such as 1, 2, 3, ... will produce output that + also has excellent statistical randomness properties but is + predictable. On the other hand, taking successive rolls of a six- + sided die and encoding the resulting values in ASCII would produce + statistically poor output with a substantial unpredictable component. + So note that passing or failing statistical tests doesn't reveal + whether something is unpredictable or predictable. + + + + + + + + +Eastlake, et al. Standards Track [Page 6] + +RFC 4086 Randomness Requirements for Security June 2005 + + +3. Entropy Sources + + Entropy sources tend to be very implementation dependent. Once one + has gathered sufficient entropy, it can be used as the seed to + produce the required amount of cryptographically strong pseudo- + randomness, as described in Sections 6 and 7, after being de-skewed + or mixed as necessary, as described in Sections 4 and 5. + + Is there any hope for true, strong, portable randomness in the + future? There might be. All that's needed is a physical source of + unpredictable numbers. + + Thermal noise (sometimes called Johnson noise in integrated circuits) + or a radioactive decay source and a fast, free-running oscillator + would do the trick directly [GIFFORD]. This is a trivial amount of + hardware, and it could easily be included as a standard part of a + computer system's architecture. Most audio (or video) input devices + are usable [TURBID]. Furthermore, any system with a spinning disk or + ring oscillator and a stable (crystal) time source or the like has an + adequate source of randomness ([DAVIS] and Section 3.3). All that's + needed is the common perception among computer vendors that this + small additional hardware and the software to access it is necessary + and useful. + + ANSI X9 is currently developing a standard that includes a part + devoted to entropy sources. See Part 2 of [X9.82]. + +3.1. Volume Required + + How much unpredictability is needed? Is it possible to quantify the + requirement in terms of, say, number of random bits per second? + + The answer is that not very much is needed. For AES, the key can be + 128 bits, and, as we show in an example in Section 8, even the + highest security system is unlikely to require strong keying material + of much over 200 bits. If a series of keys is needed, they can be + generated from a strong random seed (starting value) using a + cryptographically strong sequence, as explained in Section 6.2. A + few hundred random bits generated at start-up or once a day is enough + if such techniques are used. Even if the random bits are generated + as slowly as one per second and it is not possible to overlap the + generation process, it should be tolerable in most high-security + applications to wait 200 seconds occasionally. + + These numbers are trivial to achieve. It could be achieved by a + person repeatedly tossing a coin, and almost any hardware based + process is likely to be much faster. + + + + +Eastlake, et al. Standards Track [Page 7] + +RFC 4086 Randomness Requirements for Security June 2005 + + +3.2. Existing Hardware Can Be Used For Randomness + + As described below, many computers come with hardware that can, with + care, be used to generate truly random quantities. + +3.2.1. Using Existing Sound/Video Input + + Many computers are built with inputs that digitize some real-world + analog source, such as sound from a microphone or video input from a + camera. The "input" from a sound digitizer with no source plugged in + or from a camera with the lens cap on is essentially thermal noise. + If the system has enough gain to detect anything, such input can + provide reasonably high quality random bits. This method is + extremely dependent on the hardware implementation. + + For example, on some UNIX-based systems, one can read from the + /dev/audio device with nothing plugged into the microphone jack or + with the microphone receiving only low level background noise. Such + data is essentially random noise, although it should not be trusted + without some checking, in case of hardware failure, and it will have + to be de-skewed. + + Combining this approach with compression to de-skew (see Section 4), + one can generate a huge amount of medium-quality random data with the + UNIX-style command line: + + cat /dev/audio | compress - >random-bits-file + + A detailed examination of this type of randomness source appears in + [TURBID]. + +3.2.2. Using Existing Disk Drives + + Disk drives have small random fluctuations in their rotational speed + due to chaotic air turbulence [DAVIS, Jakobsson]. The addition of + low-level disk seek-time instrumentation produces a series of + measurements that contain this randomness. Such data is usually + highly correlated, so significant processing is needed, as described + in Section 5.2 below. Nevertheless, experimentation a decade ago + showed that, with such processing, even slow disk drives on the + slower computers of that day could easily produce 100 bits a minute + or more of excellent random data. + + Every increase in processor speed, which increases the resolution + with which disk motion can be timed or increases the rate of disk + seeks, increases the rate of random bit generation possible with this + technique. At the time of this paper and with modern hardware, a + more typical rate of random bit production would be in excess of + + + +Eastlake, et al. Standards Track [Page 8] + +RFC 4086 Randomness Requirements for Security June 2005 + + + 10,000 bits a second. This technique is used in random number + generators included in many operating system libraries. + + Note: the inclusion of cache memories in disk controllers has little + effect on this technique if very short seek times, which represent + cache hits, are simply ignored. + +3.3. Ring Oscillator Sources + + If an integrated circuit is being designed or field-programmed, an + odd number of gates can be connected in series to produce a free- + running ring oscillator. By sampling a point in the ring at a fixed + frequency (for example, one determined by a stable crystal + oscillator), some amount of entropy can be extracted due to + variations in the free-running oscillator timing. It is possible to + increase the rate of entropy by XOR'ing sampled values from a few + ring oscillators with relatively prime lengths. It is sometimes + recommended that an odd number of rings be used so that, even if the + rings somehow become synchronously locked to each other, there will + still be sampled bit transitions. Another possible source to sample + is the output of a noisy diode. + + Sampled bits from such sources will have to be heavily de-skewed, as + disk rotation timings must be (see Section 4). An engineering study + would be needed to determine the amount of entropy being produced + depending on the particular design. In any case, these can be good + sources whose cost is a trivial amount of hardware by modern + standards. + + As an example, IEEE 802.11i suggests the circuit below, with due + attention in the design to isolation of the rings from each other and + from clocked circuits to avoid undesired synchronization, etc., and + with extensive post processing [IEEE_802.11i]. + + + + + + + + + + + + + + + + + + +Eastlake, et al. Standards Track [Page 9] + +RFC 4086 Randomness Requirements for Security June 2005 + + + |\ |\ |\ + +-->| >0-->| >0-- 19 total --| >0--+-------+ + | |/ |/ |/ | | + | | | + +----------------------------------+ V + +-----+ + |\ |\ |\ | | output + +-->| >0-->| >0-- 23 total --| >0--+--->| XOR |------> + | |/ |/ |/ | | | + | | +-----+ + +----------------------------------+ ^ ^ + | | + |\ |\ |\ | | + +-->| >0-->| >0-- 29 total --| >0--+------+ | + | |/ |/ |/ | | + | | | + +----------------------------------+ | + | + Other randomness, if available ---------+ + +3.4. Problems with Clocks and Serial Numbers + + Computer clocks and similar operating system or hardware values, + provide significantly fewer real bits of unpredictability than might + appear from their specifications. + + Tests have been done on clocks on numerous systems, and it was found + that their behavior can vary widely and in unexpected ways. One + version of an operating system running on one set of hardware may + actually provide, say, microsecond resolution in a clock, while a + different configuration of the "same" system may always provide the + same lower bits and only count in the upper bits at much lower + resolution. This means that successive reads of the clock may + produce identical values even if enough time has passed that the + value "should" change based on the nominal clock resolution. There + are also cases where frequently reading a clock can produce + artificial sequential values, because of extra code that checks for + the clock being unchanged between two reads and increases it by one! + Designing portable application code to generate unpredictable numbers + based on such system clocks is particularly challenging because the + system designer does not always know the properties of the system + clock. + + Use of a hardware serial number (such as an Ethernet MAC address) may + also provide fewer bits of uniqueness than one would guess. Such + quantities are usually heavily structured, and subfields may have + only a limited range of possible values, or values may be easily + guessable based on approximate date of manufacture or other data. + + + +Eastlake, et al. Standards Track [Page 10] + +RFC 4086 Randomness Requirements for Security June 2005 + + + For example, it is likely that a company that manufactures both + computers and Ethernet adapters will, at least internally, use its + own adapters, which significantly limits the range of built-in + addresses. + + Problems such as those described above make the production of code to + generate unpredictable quantities difficult if the code is to be + ported across a variety of computer platforms and systems. + +3.5. Timing and Value of External Events + + It is possible to measure the timing and content of mouse movement, + key strokes, and similar user events. This is a reasonable source of + unguessable data, with some qualifications. On some machines, input + such as key strokes is buffered. Even though the user's inter- + keystroke timing may have sufficient variation and unpredictability, + there might not be an easy way to access that variation. Another + problem is that no standard method exists for sampling timing + details. This makes it hard to use this technique to build standard + software intended for distribution to a large range of machines. + + The amount of mouse movement and the actual key strokes are usually + easier to access than timings, but they may yield less + unpredictability because the user may provide highly repetitive + input. + + Other external events, such as network packet arrival times and + lengths, can also be used, but only with great care. In particular, + the possibility of manipulation of such network traffic measurements + by an adversary and the lack of history at system start-up must be + carefully considered. If this input is subject to manipulation, it + must not be trusted as a source of entropy. + + In principle, almost any external sensor, such as raw radio reception + or temperature sensing in appropriately equipped computers, can be + used. But in each case, careful consideration must be given to how + much this data is subject to adversarial manipulation and to how much + entropy it can actually provide. + + The above techniques are quite powerful against attackers that have + no access to the quantities being measured. For example, these + techniques would be powerful against offline attackers who had no + access to one's environment and who were trying to crack one's random + seed after the fact. In all cases, the more accurately one can + measure the timing or value of an external sensor, the more rapidly + one can generate bits. + + + + + +Eastlake, et al. Standards Track [Page 11] + +RFC 4086 Randomness Requirements for Security June 2005 + + +3.6. Non-hardware Sources of Randomness + + The best source of input entropy would be a hardware-based random + source such as ring oscillators, disk drive timing, thermal noise, or + radioactive decay. However, if none of these is available, there are + other possibilities. These include system clocks, system or + input/output buffers, user/system/hardware/network serial numbers or + addresses and timing, and user input. Unfortunately, each of these + sources can produce very limited or predictable values under some + circumstances. + + Some of the sources listed above would be quite strong on multi-user + systems, where each user of the system is in essence a source of + randomness. However, on a small single-user or embedded system, + especially at start-up, it might be possible for an adversary to + assemble a similar configuration. This could give the adversary + inputs to the mixing process that were well-enough correlated to + those used originally to make exhaustive search practical. + + The use of multiple random inputs with a strong mixing function is + recommended and can overcome weakness in any particular input. The + timing and content of requested "random" user keystrokes can yield + hundreds of random bits, but conservative assumptions need to be + made. For example, one reasonably conservative assumption would be + that an inter-keystroke interval provides at most a few bits of + randomness, but only when the interval is unique in the sequence of + intervals up to that point. A similar assumption would be that a key + code provides a few bits of randomness, but only when the code is + unique in the sequence. Thus, an interval or key code that + duplicated a previous value would be assumed to provide no additional + randomness. The results of mixing these timings with typed + characters could be further combined with clock values and other + inputs. + + This strategy may make practical portable code for producing good + random numbers for security, even if some of the inputs are very weak + on some of the target systems. However, it may still fail against a + high-grade attack on small, single-user, or embedded systems, + especially if the adversary has ever been able to observe the + generation process in the past. A hardware-based random source is + still preferable. + +4. De-skewing + + Is there any specific requirement on the shape of the distribution of + quantities gathered for the entropy to produce the random numbers? + The good news is that the distribution need not be uniform. All that + is needed to bound performance is a conservative estimate of how + + + +Eastlake, et al. Standards Track [Page 12] + +RFC 4086 Randomness Requirements for Security June 2005 + + + non-uniform it is. Simple techniques to de-skew a bit stream are + given below, and stronger cryptographic techniques are described in + Section 5.2. + +4.1. Using Stream Parity to De-Skew + + As a simple but not particularly practical example, consider taking a + sufficiently long string of bits and mapping the string to "zero" or + "one". The mapping will not yield a perfectly uniform distribution, + but it can be as close as desired. One mapping that serves the + purpose is to take the parity of the string. This has the advantages + that it is robust across all degrees of skew up to the estimated + maximum skew and that it is trivial to implement in hardware. + + The following analysis gives the number of bits that must be sampled: + + Suppose that the ratio of ones to zeros is ( 0.5 + E ) to + ( 0.5 - E ), where E is between 0 and 0.5 and is a measure of the + "eccentricity" of the distribution. Consider the distribution of the + parity function of N bit samples. The respective probabilities that + the parity will be one or zero will be the sum of the odd or even + terms in the binomial expansion of (p + q)^N, where p = 0.5 + E, the + probability of a one, and q = 0.5 - E, the probability of a zero. + + These sums can be computed easily as + + N N + 1/2 * ( ( p + q ) + ( p - q ) ) + + and + N N + 1/2 * ( ( p + q ) - ( p - q ) ). + + (Which formula corresponds to the probability that the parity will be + 1 depends on whether N is odd or even.) + + Since p + q = 1 and p - q = 2E, these expressions reduce to + + N + 1/2 * [1 + (2E) ] + + and + N + 1/2 * [1 - (2E) ]. + + Neither of these will ever be exactly 0.5 unless E is zero, but we + can bring them arbitrarily close to 0.5. If we want the + probabilities to be within some delta d of 0.5, e.g., then + + + +Eastlake, et al. Standards Track [Page 13] + +RFC 4086 Randomness Requirements for Security June 2005 + + + N + ( 0.5 + ( 0.5 * (2E) ) ) < 0.5 + d. + + Solving for N yields N > log(2d)/log(2E). (Note that 2E is less than + 1, so its log is negative. Division by a negative number reverses + the sense of an inequality.) + + The following table gives the length N of the string that must be + sampled for various degrees of skew in order to come within 0.001 of + a 50/50 distribution. + + +---------+--------+-------+ + | Prob(1) | E | N | + +---------+--------+-------+ + | 0.5 | 0.00 | 1 | + | 0.6 | 0.10 | 4 | + | 0.7 | 0.20 | 7 | + | 0.8 | 0.30 | 13 | + | 0.9 | 0.40 | 28 | + | 0.95 | 0.45 | 59 | + | 0.99 | 0.49 | 308 | + +---------+--------+-------+ + + The last entry shows that even if the distribution is skewed 99% in + favor of ones, the parity of a string of 308 samples will be within + 0.001 of a 50/50 distribution. But, as we shall see in section 5.2, + there are much stronger techniques that extract more of the available + entropy. + +4.2. Using Transition Mappings to De-Skew + + Another technique, originally due to von Neumann [VON_NEUMANN], is to + examine a bit stream as a sequence of non-overlapping pairs. One + could then discard any 00 or 11 pairs found, interpret 01 as a 0 and + 10 as a 1. Assume that the probability of a 1 is 0.5+E and that the + probability of a 0 is 0.5-E, where E is the eccentricity of the + source as described in the previous section. Then the probability of + each pair is shown in the following table: + + +------+-----------------------------------------+ + | pair | probability | + +------+-----------------------------------------+ + | 00 | (0.5 - E)^2 = 0.25 - E + E^2 | + | 01 | (0.5 - E)*(0.5 + E) = 0.25 - E^2 | + | 10 | (0.5 + E)*(0.5 - E) = 0.25 - E^2 | + | 11 | (0.5 + E)^2 = 0.25 + E + E^2 | + +------+-----------------------------------------+ + + + + +Eastlake, et al. Standards Track [Page 14] + +RFC 4086 Randomness Requirements for Security June 2005 + + + This technique will completely eliminate any bias but requires an + indeterminate number of input bits for any particular desired number + of output bits. The probability of any particular pair being + discarded is 0.5 + 2E^2, so the expected number of input bits to + produce X output bits is X/(0.25 - E^2). + + This technique assumes that the bits are from a stream where each bit + has the same probability of being a 0 or 1 as any other bit in the + stream and that bits are uncorrelated, i.e., that the bits come from + identical independent distributions. If alternate bits are from two + correlated sources, for example, the above analysis breaks down. + + The above technique also provides another illustration of how a + simple statistical analysis can mislead if one is not always on the + lookout for patterns that could be exploited by an adversary. If the + algorithm were misread slightly so that overlapping successive bits + pairs were used instead of non-overlapping pairs, the statistical + analysis given would be the same. However, instead of providing an + unbiased, uncorrelated series of random 1s and 0s, it would produce a + totally predictable sequence of exactly alternating 1s and 0s. + +4.3. Using FFT to De-Skew + + When real-world data consists of strongly correlated bits, it may + still contain useful amounts of entropy. This entropy can be + extracted through various transforms, the most powerful of which are + described in section 5.2 below. + + Using the Fourier transform of the data or its optimized variant, the + FFT, is interesting primarily for theoretical reasons. It can be + shown that this technique will discard strong correlations. If + adequate data is processed and if remaining correlations decay, + spectral lines that approach statistical independence and normally + distributed randomness can be produced [BRILLINGER]. + +4.4. Using Compression to De-Skew + + Reversible compression techniques also provide a crude method of de- + skewing a skewed bit stream. This follows directly from the + definition of reversible compression and the formula in Section 2 for + the amount of information in a sequence. Since the compression is + reversible, the same amount of information must be present in the + shorter output as was present in the longer input. By the Shannon + information equation, this is only possible if, on average, the + probabilities of the different shorter sequences are more uniformly + distributed than were the probabilities of the longer sequences. + Therefore, the shorter sequences must be de-skewed relative to the + input. + + + +Eastlake, et al. Standards Track [Page 15] + +RFC 4086 Randomness Requirements for Security June 2005 + + + However, many compression techniques add a somewhat predictable + preface to their output stream and may insert a similar sequence + periodically in their output or otherwise introduce subtle patterns + of their own. They should be considered only rough techniques + compared to those described in Section 5.2. At a minimum, the + beginning of the compressed sequence should be skipped and only later + bits should used for applications requiring roughly-random bits. + +5. Mixing + + What is the best overall strategy for obtaining unguessable random + numbers in the absence of a strong, reliable hardware entropy source? + It is to obtain input from a number of uncorrelated sources and to + mix them with a strong mixing function. Such a function will + preserve the entropy present in any of the sources, even if other + quantities being combined happen to be fixed or easily guessable (low + entropy). This approach may be advisable even with a good hardware + source, as hardware can also fail. However, this should be weighed + against a possible increase in the chance of overall failure due to + added software complexity. + + Once one has used good sources, such as some of those listed in + Section 3, and mixed them as described in this section, one has a + strong seed. This can then be used to produce large quantities of + cryptographically strong material as described in Sections 6 and 7. + + A strong mixing function is one that combines inputs and produces an + output in which each output bit is a different complex non-linear + function of all the input bits. On average, changing any input bit + will change about half the output bits. But because the relationship + is complex and non-linear, no particular output bit is guaranteed to + change when any particular input bit is changed. + + Consider the problem of converting a stream of bits that is skewed + towards 0 or 1 or which has a somewhat predictable pattern to a + shorter stream which is more random, as discussed in Section 4. This + is simply another case where a strong mixing function is desired, to + mix the input bits and produce a smaller number of output bits. The + technique given in Section 4.1, using the parity of a number of bits, + is simply the result of successively XORing them. This is examined + as a trivial mixing function, immediately below. Use of stronger + mixing functions to extract more of the randomness in a stream of + skewed bits is examined in Section 5.2. See also [NASLUND]. + + + + + + + + +Eastlake, et al. Standards Track [Page 16] + +RFC 4086 Randomness Requirements for Security June 2005 + + +5.1. A Trivial Mixing Function + + For expository purposes we describe a trivial example for single bit + inputs using the Exclusive Or (XOR) function. This function is + equivalent to addition without carry, as show in the table below. + This is a degenerate case in which the one output bit always changes + for a change in either input bit. But, despite its simplicity, it + provides a useful illustration. + + +-----------+-----------+----------+ + | input 1 | input 2 | output | + +-----------+-----------+----------+ + | 0 | 0 | 0 | + | 0 | 1 | 1 | + | 1 | 0 | 1 | + | 1 | 1 | 0 | + +-----------+-----------+----------+ + + If inputs 1 and 2 are uncorrelated and combined in this fashion, then + the output will be an even better (less skewed) random bit than the + inputs are. If we assume an "eccentricity" E as defined in Section + 4.1 above, then the output eccentricity relates to the input + eccentricity as follows: + + E = 2 * E * E + output input 1 input 2 + + Since E is never greater than 1/2, the eccentricity is always + improved, except in the case in which at least one input is a totally + skewed constant. This is illustrated in the following table, where + the top and left side values are the two input eccentricities and the + entries are the output eccentricity: + + +--------+--------+--------+--------+--------+--------+--------+ + | E | 0.00 | 0.10 | 0.20 | 0.30 | 0.40 | 0.50 | + +--------+--------+--------+--------+--------+--------+--------+ + | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | + | 0.10 | 0.00 | 0.02 | 0.04 | 0.06 | 0.08 | 0.10 | + | 0.20 | 0.00 | 0.04 | 0.08 | 0.12 | 0.16 | 0.20 | + | 0.30 | 0.00 | 0.06 | 0.12 | 0.18 | 0.24 | 0.30 | + | 0.40 | 0.00 | 0.08 | 0.16 | 0.24 | 0.32 | 0.40 | + | 0.50 | 0.00 | 0.10 | 0.20 | 0.30 | 0.40 | 0.50 | + +--------+--------+--------+--------+--------+--------+--------+ + + However, note that the above calculations assume that the inputs are + not correlated. If the inputs were, say, the parity of the number of + minutes from midnight on two clocks accurate to a few seconds, then + each might appear random if sampled at random intervals much longer + + + +Eastlake, et al. Standards Track [Page 17] + +RFC 4086 Randomness Requirements for Security June 2005 + + + than a minute. Yet if they were both sampled and combined with XOR, + the result would be zero most of the time. + +5.2. Stronger Mixing Functions + + The US Government Advanced Encryption Standard [AES] is an example of + a strong mixing function for multiple bit quantities. It takes up to + 384 bits of input (128 bits of "data" and 256 bits of "key") and + produces 128 bits of output, each of which is dependent on a complex + non-linear function of all input bits. Other encryption functions + with this characteristic, such as [DES], can also be used by + considering them to mix all of their key and data input bits. + + Another good family of mixing functions is the "message digest" or + hashing functions such as the US Government Secure Hash Standards + [SHA*] and the MD4, MD5 [MD4, MD5] series. These functions all take + a practically unlimited amount of input and produce a relatively + short fixed-length output mixing all the input bits. The MD* series + produces 128 bits of output, SHA-1 produces 160 bits, and other SHA + functions produce up to 512 bits. + + Although the message digest functions are designed for variable + amounts of input, AES and other encryption functions can also be used + to combine any number of inputs. If 128 bits of output is adequate, + the inputs can be packed into a 128-bit data quantity and successive + AES "keys", padding with zeros if needed; the quantity is then + successively encrypted by the "keys" using AES in Electronic Codebook + Mode. Alternatively, the input could be packed into one 128-bit key + and multiple data blocks and a CBC-MAC could be calculated [MODES]. + + More complex mixing should be used if more than 128 bits of output + are needed and one wants to employ AES (but note that it is + absolutely impossible to get more bits of "randomness" out than are + put in). For example, suppose that inputs are packed into three + quantities, A, B, and C. One may use AES to encrypt A with B and + then with C as keys to produce the first part of the output, then + encrypt B with C and then A for more output and, if necessary, + encrypt C with A and then B for yet more output. Still more output + can be produced by reversing the order of the keys given above. The + same can be done with the hash functions, hashing various subsets of + the input data or different copies of the input data with different + prefixes and/or suffixes to produce multiple outputs. + + For an example of using a strong mixing function, reconsider the case + of a string of 308 bits, each of which is biased 99% toward zero. + The parity technique given in Section 4.1 reduces this to one bit, + with only a 1/1000 deviance from being equally likely a zero or one. + But, applying the equation for information given in Section 2, this + + + +Eastlake, et al. Standards Track [Page 18] + +RFC 4086 Randomness Requirements for Security June 2005 + + + 308-bit skewed sequence contains over 5 bits of information. Thus, + hashing it with SHA-1 and taking the bottom 5 bits of the result + would yield 5 unbiased random bits and not the single bit given by + calculating the parity of the string. Alternatively, for some + applications, you could use the entire hash output to retain almost + all of the 5+ bits of entropy in a 160-bit quantity. + +5.3. Using S-Boxes for Mixing + + Many modern block encryption functions, including DES and AES, + incorporate modules known as S-Boxes (substitution boxes). These + produce a smaller number of outputs from a larger number of inputs + through a complex non-linear mixing function that has the effect of + concentrating limited entropy from the inputs into the output. + + S-Boxes sometimes incorporate bent Boolean functions (functions of an + even number of bits producing one output bit with maximum non- + linearity). Looking at the output for all input pairs differing in + any particular bit position, exactly half the outputs are different. + An S-Box in which each output bit is produced by a bent function such + that any linear combination of these functions is also a bent + function is called a "perfect S-Box". + + S-boxes and various repeated applications or cascades of such boxes + can be used for mixing [SBOX1, SBOX2]. + +5.4. Diffie-Hellman as a Mixing Function + + Diffie-Hellman exponential key exchange is a technique that yields a + shared secret between two parties. It can be computationally + infeasible for a third party to determine this secret even if they + can observe all the messages between the two communicating parties. + This shared secret is a mixture of initial quantities generated by + each of the parties [D-H]. + + If these initial quantities are random and uncorrelated, then the + shared secret combines their entropy but, of course, can not produce + more randomness than the size of the shared secret generated. + + Although this is true if the Diffie-Hellman computation is performed + privately, an adversary who can observe either of the public keys and + knows the modulus being used need only search through the space of + the other secret key in order to be able to calculate the shared + secret [D-H]. So, conservatively, it would be best to consider + public Diffie-Hellman to produce a quantity whose guessability + corresponds to the worse of the two inputs. Because of this and the + fact that Diffie-Hellman is computationally intensive, its use as a + mixing function is not recommended. + + + +Eastlake, et al. Standards Track [Page 19] + +RFC 4086 Randomness Requirements for Security June 2005 + + +5.5. Using a Mixing Function to Stretch Random Bits + + Although it is not necessary for a mixing function to produce the + same or fewer output bits than its inputs, mixing bits cannot + "stretch" the amount of random unpredictability present in the + inputs. Thus, four inputs of 32 bits each, in which there are 12 + bits worth of unpredictability (such as 4,096 equally probable + values) in each input, cannot produce more than 48 bits worth of + unpredictable output. The output can be expanded to hundreds or + thousands of bits by, for example, mixing with successive integers, + but the clever adversary's search space is still 2^48 possibilities. + Furthermore, mixing to fewer bits than are input will tend to + strengthen the randomness of the output. + + The last table in Section 5.1 shows that mixing a random bit with a + constant bit with Exclusive Or will produce a random bit. While this + is true, it does not provide a way to "stretch" one random bit into + more than one. If, for example, a random bit is mixed with a 0 and + then with a 1, this produces a two bit sequence but it will always be + either 01 or 10. Since there are only two possible values, there is + still only the one bit of original randomness. + +5.6. Other Factors in Choosing a Mixing Function + + For local use, AES has the advantages that it has been widely tested + for flaws, is reasonably efficient in software, and is widely + documented and implemented with hardware and software implementations + available all over the world including open source code. The SHA* + family have had a little less study and tend to require more CPU + cycles than AES but there is no reason to believe they are flawed. + Both SHA* and MD5 were derived from the earlier MD4 algorithm. They + all have source code available [SHA*, MD4, MD5]. Some signs of + weakness have been found in MD4 and MD5. In particular, MD4 has only + three rounds and there are several independent breaks of the first + two or last two rounds. And some collisions have been found in MD5 + output. + + AES was selected by a robust, public, and international process. It + and SHA* have been vouched for by the US National Security Agency + (NSA) on the basis of criteria that mostly remain secret, as was DES. + While this has been the cause of much speculation and doubt, + investigation of DES over the years has indicated that NSA + involvement in modifications to its design, which originated with + IBM, was primarily to strengthen it. There has been no announcement + of a concealed or special weakness being found in DES. It is likely + that the NSA modifications to MD4 to produce the SHA algorithms + similarly strengthened these algorithms, possibly against threats not + yet known in the public cryptographic community. + + + +Eastlake, et al. Standards Track [Page 20] + +RFC 4086 Randomness Requirements for Security June 2005 + + + Where input lengths are unpredictable, hash algorithms are more + convenient to use than block encryption algorithms since they are + generally designed to accept variable length inputs. Block + encryption algorithms generally require an additional padding + algorithm to accommodate inputs that are not an even multiple of the + block size. + + As of the time of this document, the authors know of no patent claims + to the basic AES, DES, SHA*, MD4, and MD5 algorithms other than + patents for which an irrevocable royalty free license has been + granted to the world. There may, of course, be essential patents of + which the authors are unaware or patents on implementations or uses + or other relevant patents issued or to be issued. + +6. Pseudo-random Number Generators + + When a seed has sufficient entropy, from input as described in + Section 3 and possibly de-skewed and mixed as described in Sections 4 + and 5, one can algorithmically extend that seed to produce a large + number of cryptographically-strong random quantities. Such + algorithms are platform independent and can operate in the same + fashion on any computer. For the algorithms to be secure, their + input and internal workings must be protected from adversarial + observation. + + The design of such pseudo-random number generation algorithms, like + the design of symmetric encryption algorithms, is not a task for + amateurs. Section 6.1 below lists a number of bad ideas that failed + algorithms have used. To learn what works, skip Section 6.1 and just + read the remainder of this section and Section 7, which describes and + references some standard pseudo random number generation algorithms. + See Section 7 and Part 3 of [X9.82]. + +6.1. Some Bad Ideas + + The subsections below describe a number of ideas that might seem + reasonable but that lead to insecure pseudo-random number generation. + +6.1.1. The Fallacy of Complex Manipulation + + One approach that may give a misleading appearance of + unpredictability is to take a very complex algorithm (or an excellent + traditional pseudo-random number generator with good statistical + properties) and to calculate a cryptographic key by starting with + limited data such as the computer system clock value as the seed. + Adversaries who knew roughly when the generator was started would + have a relatively small number of seed values to test, as they would + know likely values of the system clock. Large numbers of pseudo- + + + +Eastlake, et al. Standards Track [Page 21] + +RFC 4086 Randomness Requirements for Security June 2005 + + + random bits could be generated, but the search space that an + adversary would need to check could be quite small. + + Thus, very strong or complex manipulation of data will not help if + the adversary can learn what the manipulation is and if there is not + enough entropy in the starting seed value. They can usually use the + limited number of results stemming from a limited number of seed + values to defeat security. + + Another serious strategic error is to assume that a very complex + pseudo-random number generation algorithm will produce strong random + numbers, when there has been no theory behind or analysis of the + algorithm. There is a excellent example of this fallacy near the + beginning of Chapter 3 in [KNUTH], where the author describes a + complex algorithm. It was intended that the machine language program + corresponding to the algorithm would be so complicated that a person + trying to read the code without comments wouldn't know what the + program was doing. Unfortunately, actual use of this algorithm + showed that it almost immediately converged to a single repeated + value in one case and a small cycle of values in another case. + + Not only does complex manipulation not help you if you have a limited + range of seeds, but blindly-chosen complex manipulation can destroy + the entropy in a good seed! + +6.1.2. The Fallacy of Selection from a Large Database + + Another approach that can give a misleading appearance of + unpredictability is to randomly select a quantity from a database and + to assume that its strength is related to the total number of bits in + the database. For example, typical USENET servers process many + megabytes of information per day [USENET_1, USENET_2]. Assume that a + random quantity was selected by fetching 32 bytes of data from a + random starting point in this data. This does not yield 32*8 = 256 + bits worth of unguessability. Even if much of the data is human + language that contains no more than 2 or 3 bits of information per + byte, it doesn't yield 32*2 = 64 bits of unguessability. For an + adversary with access to the same Usenet database, the unguessability + rests only on the starting point of the selection. That is perhaps a + little over a couple of dozen bits of unguessability. + + The same argument applies to selecting sequences from the data on a + publicly available CD/DVD recording or any other large public + database. If the adversary has access to the same database, this + "selection from a large volume of data" step buys little. However, + if a selection can be made from data to which the adversary has no + access, such as system buffers on an active multi-user system, it may + be of help. + + + +Eastlake, et al. Standards Track [Page 22] + +RFC 4086 Randomness Requirements for Security June 2005 + + +6.1.3. Traditional Pseudo-random Sequences + + This section talks about traditional sources of deterministic or + "pseudo-random" numbers. These typically start with a "seed" + quantity and use simple numeric or logical operations to produce a + sequence of values. Note that none of the techniques discussed in + this section is suitable for cryptographic use. They are presented + for general information. + + [KNUTH] has a classic exposition on pseudo-random numbers. + Applications he mentions are simulations of natural phenomena, + sampling, numerical analysis, testing computer programs, decision + making, and games. None of these have the same characteristics as + the sorts of security uses we are talking about. Only in the last + two could there be an adversary trying to find the random quantity. + However, in these cases, the adversary normally has only a single + chance to use a guessed value. In guessing passwords or attempting + to break an encryption scheme, the adversary normally has many, + perhaps unlimited, chances at guessing the correct value. Sometimes + the adversary can store the message to be broken and repeatedly + attack it. Adversaries are also be assumed to be aided by a + computer. + + For testing the "randomness" of numbers, Knuth suggests a variety of + measures, including statistical and spectral. These tests check + things like autocorrelation between different parts of a "random" + sequence or distribution of its values. But these tests could be met + by a constant stored random sequence, such as the "random" sequence + printed in the CRC Standard Mathematical Tables [CRC]. Despite + meeting all the tests suggested by Knuth, that sequence is unsuitable + for cryptographic us, as adversaries must be assumed to have copies + of all commonly published "random" sequences and to be able to spot + the source and predict future values. + + A typical pseudo-random number generation technique is the linear + congruence pseudo-random number generator. This technique uses + modular arithmetic, where the value numbered N+1 is calculated from + the value numbered N by + + V = ( V * a + b )(Mod c) + N+1 N + + The above technique has a strong relationship to linear shift + register pseudo-random number generators, which are well understood + cryptographically [SHIFT*]. In such generators, bits are introduced + at one end of a shift register as the Exclusive Or (binary sum + without carry) of bits from selected fixed taps into the register. + For example, consider the following: + + + +Eastlake, et al. Standards Track [Page 23] + +RFC 4086 Randomness Requirements for Security June 2005 + + + +----+ +----+ +----+ +----+ + | B | <-- | B | <-- | B | <-- . . . . . . <-- | B | <-+ + | 0 | | 1 | | 2 | | n | | + +----+ +----+ +----+ +----+ | + | | | | + | | V +-----+ + | V +----------------> | | + V +-----------------------------> | XOR | + +---------------------------------------------------> | | + +-----+ + + V = ( ( V * 2 ) + B XOR B ... )(Mod 2^n) + N+1 N 0 2 + + The quality of traditional pseudo-random number generator algorithms + is measured by statistical tests on such sequences. Carefully-chosen + values a, b, c, and initial V or carefully-chosen placement of the + shift register tap in the above simple process can produce excellent + statistics. + + These sequences may be adequate in simulations (Monte Carlo + experiments) as long as the sequence is orthogonal to the structure + of the space being explored. Even there, subtle patterns may cause + problems. However, such sequences are clearly bad for use in + security applications. They are fully predictable if the initial + state is known. Depending on the form of the pseudo-random number + generator, the sequence may be determinable from observation of a + short portion of the sequence [SCHNEIER, STERN]. For example, with + the generators above, one can determine V(n+1) given knowledge of + V(n). In fact, it has been shown that with these techniques, even if + only one bit of the pseudo-random values are released, the seed can + be determined from short sequences. + + Not only have linear congruent generators been broken, but techniques + are now known for breaking all polynomial congruent generators + [KRAWCZYK]. + +6.2. Cryptographically Strong Sequences + + In cases where a series of random quantities must be generated, an + adversary may learn some values in the sequence. In general, + adversaries should not be able to predict other values from the ones + that they know. + + The correct technique is to start with a strong random seed, to take + cryptographically strong steps from that seed [FERGUSON, SCHNEIER], + and not to reveal the complete state of the generator in the sequence + elements. If each value in the sequence can be calculated in a fixed + + + +Eastlake, et al. Standards Track [Page 24] + +RFC 4086 Randomness Requirements for Security June 2005 + + + way from the previous value, then when any value is compromised, all + future values can be determined. This would be the case, for + example, if each value were a constant function of the previously + used values, even if the function were a very strong, non-invertible + message digest function. + + (Note that if a technique for generating a sequence of key values is + fast enough, it can trivially be used as the basis for a + confidentiality system. If two parties use the same sequence + generation technique and start with the same seed material, they will + generate identical sequences. These could, for example, be XOR'ed at + one end with data being sent to encrypt it, and XOR'ed with this data + as received to decrypt it, due to the reversible properties of the + XOR operation. This is commonly referred to as a simple stream + cipher.) + +6.2.1. OFB and CTR Sequences + + One way to produce a strong sequence is to take a seed value and hash + the quantities produced by concatenating the seed with successive + integers, or the like, and then to mask the values obtained so as to + limit the amount of generator state available to the adversary. + + It may also be possible to use an "encryption" algorithm with a + random key and seed value to encrypt successive integers, as in + counter (CTR) mode encryption. Alternatively, one can feedback all + of the output value from encryption into the value to be encrypted + for the next iteration. This is a particular example of output + feedback mode (OFB) [MODES]. + + An example is shown below in which shifting and masking are used to + combine part of the output feedback with part of the old input. This + type of partial feedback should be avoided for reasons described + below. + + + + + + + + + + + + + + + + + +Eastlake, et al. Standards Track [Page 25] + +RFC 4086 Randomness Requirements for Security June 2005 + + + +---------------+ + | V | + | | n |--+ + +--+------------+ | + | | +---------+ + shift| +---> | | +-----+ + +--+ | Encrypt | <--- | Key | + | +-------- | | +-----+ + | | +---------+ + V V + +------------+--+ + | V | | + | n+1 | + +---------------+ + + Note that if a shift of one is used, this is the same as the shift + register technique described in Section 6.1.3, but with the all- + important difference that the feedback is determined by a complex + non-linear function of all bits rather than by a simple linear or + polynomial combination of output from a few bit position taps. + + Donald W. Davies showed that this sort of shifted partial output + feedback significantly weakens an algorithm, compared to feeding all + the output bits back as input. In particular, for DES, repeatedly + encrypting a full 64-bit quantity will give an expected repeat in + about 2^63 iterations. Feeding back anything less than 64 (and more + than 0) bits will give an expected repeat in between 2^31 and 2^32 + iterations! + + To predict values of a sequence from others when the sequence was + generated by these techniques is equivalent to breaking the + cryptosystem or to inverting the "non-invertible" hashing with only + partial information available. The less information revealed in each + iteration, the harder it will be for an adversary to predict the + sequence. Thus it is best to use only one bit from each value. It + has been shown that in some cases this makes it impossible to break a + system even when the cryptographic system is invertible and could be + broken if all of each generated value were revealed. + +6.2.2. The Blum Blum Shub Sequence Generator + + Currently the generator which has the strongest public proof of + strength is called the Blum Blum Shub generator, named after its + inventors [BBS]. It is also very simple and is based on quadratic + residues. Its only disadvantage is that it is computationally + intensive compared to the traditional techniques given in Section + 6.1.3. This is not a major drawback if it is used for moderately- + infrequent purposes, such as generating session keys. + + + +Eastlake, et al. Standards Track [Page 26] + +RFC 4086 Randomness Requirements for Security June 2005 + + + Simply choose two large prime numbers (say, p and q) that each gives + a remainder of 3 when divided by 4. Let n = p * q. Then choose a + random number, x, that is relatively prime to n. The initial seed + for the generator and the method for calculating subsequent values + are then: + + 2 + s = ( x )(Mod n) + 0 + 2 + s = ( s )(Mod n) + i+1 i + + Be careful to use only a few bits from the bottom of each s. It is + always safe to use only the lowest-order bit. If one uses no more + than the: + + log ( log ( s ) ) + 2 2 i + + low-order bits, then predicting any additional bits from a sequence + generated in this manner is provably as hard as factoring n. As long + as the initial x is secret, n can be made public if desired. + + An interesting characteristic of this generator is that any of the s + values can be directly calculated. In particular, + + ( (2^i) (Mod ((p-1)*(q-1)) ) ) + s = ( s )(Mod n) + i 0 + + This means that in applications where many keys are generated in this + fashion, it is not necessary to save them all. Each key can be + effectively indexed and recovered from that small index and the + initial s and n. + +6.3. Entropy Pool Techniques + + Many modern pseudo-random number sources, such as those described in + Sections 7.1.2 and 7.1.3 utilize the technique of maintaining a + "pool" of bits and providing operations for strongly mixing input + with some randomness into the pool and extracting pseudo-random bits + from the pool. This is illustrated in the figure below. + + + + + + + + +Eastlake, et al. Standards Track [Page 27] + +RFC 4086 Randomness Requirements for Security June 2005 + + + +--------+ +------+ +---------+ + --->| Mix In |--->| POOL |--->| Extract |---> + | Bits | | | | Bits | + +--------+ +------+ +---------+ + ^ V + | | + +-----------+ + + Bits to be fed into the pool can come from any of the various + hardware, environmental, or user input sources discussed above. It + is also common to save the state of the pool on system shutdown and + to restore it on re-starting, when stable storage is available. + + Care must be taken that enough entropy has been added to the pool to + support particular output uses desired. See [RSA_BULL1] for similar + suggestions. + +7. Randomness Generation Examples and Standards + + Several public standards and widely deployed examples are now in + place for the generation of keys or other cryptographically random + quantities. Some, in section 7.1, include an entropy source. + Others, described in section 7.2, provide the pseudo-random number + strong-sequence generator but assume the input of a random seed or + input from a source of entropy. + +7.1. Complete Randomness Generators + + Three standards are described below. The two older standards use + DES, with its 64-bit block and key size limit, but any equally strong + or stronger mixing function could be substituted [DES]. The third is + a more modern and stronger standard based on SHA-1 [SHA*]. Lastly, + the widely deployed modern UNIX and Windows random number generators + are described. + +7.1.1. US DoD Recommendations for Password Generation + + The United States Department of Defense has specific recommendations + for password generation [DoD]. It suggests using the US Data + Encryption Standard [DES] in Output Feedback Mode [MODES] as follows: + + + + + + + + + + + +Eastlake, et al. Standards Track [Page 28] + +RFC 4086 Randomness Requirements for Security June 2005 + + + Use an initialization vector determined from + the system clock, + system ID, + user ID, and + date and time; + use a key determined from + system interrupt registers, + system status registers, and + system counters; and, + as plain text, use an external randomly generated 64-bit + quantity such as the ASCII bytes for 8 characters typed + in by a system administrator. + + The password can then be calculated from the 64 bit "cipher text" + generated by DES in 64-bit Output Feedback Mode. As many bits as are + needed can be taken from these 64 bits and expanded into a + pronounceable word, phrase, or other format if a human being needs to + remember the password. + +7.1.2. The /dev/random Device + + Several versions of the UNIX operating system provide a kernel- + resident random number generator. Some of these generators use + events captured by the Kernel during normal system operation. + + For example, on some versions of Linux, the generator consists of a + random pool of 512 bytes represented as 128 words of 4 bytes each. + When an event occurs, such as a disk drive interrupt, the time of the + event is XOR'ed into the pool, and the pool is stirred via a + primitive polynomial of degree 128. The pool itself is treated as a + ring buffer, with new data being XOR'ed (after stirring with the + polynomial) across the entire pool. + + Each call that adds entropy to the pool estimates the amount of + likely true entropy the input contains. The pool itself contains a + accumulator that estimates the total over all entropy of the pool. + + Input events come from several sources, as listed below. + Unfortunately, for server machines without human operators, the first + and third are not available, and entropy may be added slowly in that + case. + + 1. Keyboard interrupts. The time of the interrupt and the scan code + are added to the pool. This in effect adds entropy from the human + operator by measuring inter-keystroke arrival times. + + 2. Disk completion and other interrupts. A system being used by a + person will likely have a hard-to-predict pattern of disk + + + +Eastlake, et al. Standards Track [Page 29] + +RFC 4086 Randomness Requirements for Security June 2005 + + + accesses. (But not all disk drivers support capturing this timing + information with sufficient accuracy to be useful.) + + 3. Mouse motion. The timing and mouse position are added in. + + When random bytes are required, the pool is hashed with SHA-1 [SHA*] + to yield the returned bytes of randomness. If more bytes are + required than the output of SHA-1 (20 bytes), then the hashed output + is stirred back into the pool and a new hash is performed to obtain + the next 20 bytes. As bytes are removed from the pool, the estimate + of entropy is correspondingly decremented. + + To ensure a reasonably random pool upon system startup, the standard + startup and shutdown scripts save the pool to a disk file at shutdown + and read this file at system startup. + + There are two user-exported interfaces. /dev/random returns bytes + from the pool but blocks when the estimated entropy drops to zero. + As entropy is added to the pool from events, more data becomes + available via /dev/random. Random data obtained from such a + /dev/random device is suitable for key generation for long term keys, + if enough random bits are in the pool or are added in a reasonable + amount of time. + + /dev/urandom works like /dev/random; however, it provides data even + when the entropy estimate for the random pool drops to zero. This + may be adequate for session keys or for other key generation tasks + for which blocking to await more random bits is not acceptable. The + risk of continuing to take data even when the pool's entropy estimate + is small in that past output may be computable from current output, + provided that an attacker can reverse SHA-1. Given that SHA-1 is + designed to be non-invertible, this is a reasonable risk. + + To obtain random numbers under Linux, Solaris, or other UNIX systems + equipped with code as described above, all an application has to do + is open either /dev/random or /dev/urandom and read the desired + number of bytes. + + (The Linux Random device was written by Theodore Ts'o. It was based + loosely on the random number generator in PGP 2.X and PGP 3.0 (aka + PGP 5.0).) + +7.1.3. Windows CryptGenRandom + + Microsoft's recommendation to users of the widely deployed Windows + operating system is generally to use the CryptGenRandom pseudo-random + number generation call with the CryptAPI cryptographic service + provider. This takes a handle to a cryptographic service provider + + + +Eastlake, et al. Standards Track [Page 30] + +RFC 4086 Randomness Requirements for Security June 2005 + + + library, a pointer to a buffer by which the caller can provide + entropy and into which the generated pseudo-randomness is returned, + and an indication of how many octets of randomness are desired. + + The Windows CryptAPI cryptographic service provider stores a seed + state variable with every user. When CryptGenRandom is called, this + is combined with any randomness provided in the call and with various + system and user data such as the process ID, thread ID, system clock, + system time, system counter, memory status, free disk clusters, and + hashed user environment block. This data is all fed to SHA-1, and + the output is used to seed an RC4 key stream. That key stream is + used to produce the pseudo-random data requested and to update the + user's seed state variable. + + Users of Windows ".NET" will probably find it easier to use the + RNGCryptoServiceProvider.GetBytes method interface. + + For further information, see [WSC]. + +7.2. Generators Assuming a Source of Entropy + + The pseudo-random number generators described in the following three + sections all assume that a seed value with sufficient entropy is + provided to them. They then generate a strong sequence (see Section + 6.2) from that seed. + +7.2.1. X9.82 Pseudo-Random Number Generation + + The ANSI X9F1 committee is in the final stages of creating a standard + for random number generation covering both true randomness generators + and pseudo-random number generators. It includes a number of + pseudo-random number generators based on hash functions, one of which + will probably be based on HMAC SHA hash constructs [RFC2104]. The + draft version of this generator is described below, omitting a number + of optional features [X9.82]. + + In the subsections below, the HMAC hash construct is simply referred + to as HMAC but, of course, a particular standard SHA function must be + selected in an particular use. Generally speaking, if the strength + of the pseudo-random values to be generated is to be N bits, the SHA + function chosen must generate N or more bits of output, and a source + of at least N bits of input entropy will be required. The same hash + function must be used throughout an instantiation of this generator. + + + + + + + + +Eastlake, et al. Standards Track [Page 31] + +RFC 4086 Randomness Requirements for Security June 2005 + + +7.2.1.1. Notation + + In the following sections, the notation give below is used: + + hash_length is the output size of the underlying hash function in + use. + + input_entropy is the input bit string that provides entropy to the + generator. + + K is a bit string of size hash_length that is part of the state of + the generator and is updated at least once each time random + bits are generated. + + V is a bit string of size hash_length and is part of the state of + the generator. It is updated each time hash_length bits of + output are generated. + + "|" represents concatenation. + +7.2.1.2. Initializing the Generator + + Set V to all zero bytes, except the low-order bit of each byte is set + to one. + + Set K to all zero bytes, then set: + + K = HMAC ( K, V | 0x00 | input_entropy ) + + V = HMAC ( K, V ) + + K = HMAC ( K, V | 0x01 | input_entropy ) + + V = HMAC ( K, V ) + + Note: All SHA algorithms produce an integral number of bytes, so the + lengths of K and V will be integral numbers of bytes. + +7.2.1.3. Generating Random Bits + + When output is called for, simply set: + + V = HMAC ( K, V ) + + and use the leading bits from V. If more bits are needed than the + length of V, set "temp" to a null bit string and then repeatedly + perform: + + + + +Eastlake, et al. Standards Track [Page 32] + +RFC 4086 Randomness Requirements for Security June 2005 + + + V = HMAC ( K, V ) + temp = temp | V + + stopping as soon as temp is equal to or longer than the number of + random bits requested. Use the requested number of leading bits from + temp. The definition of the algorithm prohibits requesting more than + 2^35 bits. + + After extracting and saving the pseudo-random output bits as + described above, before returning you must also perform two more + HMACs as follows: + + K = HMAC ( K, V | 0x00 ) + V = HMAC ( K, V ) + +7.2.2. X9.17 Key Generation + + The American National Standards Institute has specified the + following method for generating a sequence of keys [X9.17]: + + s is the initial 64 bit seed. + 0 + + g is the sequence of generated 64-bit key quantities + n + + k is a random key reserved for generating this key sequence. + + t is the time at which a key is generated, to as fine a resolution + as is available (up to 64 bits). + + DES ( K, Q ) is the DES encryption of quantity Q with key K. + + Then: + + g = DES ( k, DES ( k, t ) XOR s ) + n n + + s = DES ( k, DES ( k, t ) XOR g ) + n+1 n + + + If g sub n is to be used as a DES key, then every eighth bit should + be adjusted for parity for that use, but the entire 64 bit unmodified + g should be used in calculating the next s. + + + + + + +Eastlake, et al. Standards Track [Page 33] + +RFC 4086 Randomness Requirements for Security June 2005 + + +7.2.3. DSS Pseudo-random Number Generation + + Appendix 3 of the NIST Digital Signature Standard [DSS] provides a + method of producing a sequence of pseudo-random 160 bit quantities + for use as private keys or the like. This has been modified by + Change Notice 1 [DSS_CN1] to produce the following algorithm for + generating general-purpose pseudo-random numbers: + + t = 0x 67452301 EFCDAB89 98BADCFE 10325476 C3D2E1F0 + + XKEY = initial seed + 0 + + For j = 0 to ... + + XVAL = ( XKEY + optional user input ) (Mod 2^512) + j + + X = G( t, XVAL ) + j + + XKEY = ( 1 + XKEY + X ) (Mod 2^512) + j+1 j j + + + The quantities X thus produced are the pseudo-random sequence of + 160-bit values. Two functions can be used for "G" above. Each + produces a 160-bit value and takes two arguments, a 160-bit value and + a 512 bit value. + + The first is based on SHA-1 and works by setting the 5 linking + variables, denoted H with subscripts in the SHA-1 specification, to + the first argument divided into fifths. Then steps (a) through (e) + of section 7 of the NIST SHA-1 specification are run over the second + argument as if it were a 512-bit data block. The values of the + linking variable after those steps are then concatenated to produce + the output of G [SHA*]. + + As an alternative method, NIST also defined an alternate G function + based on multiple applications of the DES encryption function [DSS]. + +8. Examples of Randomness Required + + Below are two examples showing rough calculations of randomness + needed for security. The first is for moderate security passwords, + while the second assumes a need for a very high-security + cryptographic key. + + + + +Eastlake, et al. Standards Track [Page 34] + +RFC 4086 Randomness Requirements for Security June 2005 + + + In addition, [ORMAN] and [RSA_BULL13] provide information on the + public key lengths that should be used for exchanging symmetric keys. + +8.1. Password Generation + + Assume that user passwords change once a year and that it is desired + that the probability that an adversary could guess the password for a + particular account be less than one in a thousand. Further assume + that sending a password to the system is the only way to try a + password. Then the crucial question is how often an adversary can + try possibilities. Assume that delays have been introduced into a + system so that an adversary can make at most one password try every + six seconds. That's 600 per hour, or about 15,000 per day, or about + 5,000,000 tries in a year. Assuming any sort of monitoring, it is + unlikely that someone could actually try continuously for a year. + Even if log files are only checked monthly, 500,000 tries is more + plausible before the attack is noticed and steps are taken to change + passwords and make it harder to try more passwords. + + To have a one-in-a-thousand chance of guessing the password in + 500,000 tries implies a universe of at least 500,000,000 passwords, + or about 2^29. Thus, 29 bits of randomness are needed. This can + probably be achieved by using the US DoD-recommended inputs for + password generation, as it has 8 inputs that probably average over 5 + bits of randomness each (see section 7.1). Using a list of 1,000 + words, the password could be expressed as a three-word phrase + (1,000,000,000 possibilities). By using case-insensitive letters and + digits, six characters would suffice ((26+10)^6 = 2,176,782,336 + possibilities). + + For a higher-security password, the number of bits required goes up. + To decrease the probability by 1,000 requires increasing the universe + of passwords by the same factor, which adds about 10 bits. Thus, to + have only a one in a million chance of a password being guessed under + the above scenario would require 39 bits of randomness and a password + that was a four-word phrase from a 1,000 word list, or eight + letters/digits. To go to a one-in-10^9 chance, 49 bits of randomness + are needed, implying a five-word phrase or a ten-letter/digit + password. + + In a real system, of course, there are other factors. For example, + the larger and harder to remember passwords are, the more likely + users will bed to write them down, resulting in an additional risk of + compromise. + + + + + + + +Eastlake, et al. Standards Track [Page 35] + +RFC 4086 Randomness Requirements for Security June 2005 + + +8.2. A Very High Security Cryptographic Key + + Assume that a very high security key is needed for symmetric + encryption/decryption between two parties. Assume also that an + adversary can observe communications and knows the algorithm being + used. Within the field of random possibilities, the adversary can + try key values in hopes of finding the one in use. Assume further + that brute force trial of keys is the best the adversary can do. + +8.2.1. Effort per Key Trial + + How much effort will it take to try each key? For very high-security + applications, it is best to assume a low value of effort. Even if it + would clearly take tens of thousands of computer cycles or more to + try a single key, there may be some pattern that enables huge blocks + of key values to be tested with much less effort per key. Thus, it + is probably best to assume no more than a couple of hundred cycles + per key. (There is no clear lower bound on this, as computers + operate in parallel on a number of bits and a poor encryption + algorithm could allow many keys or even groups of keys to be tested + in parallel. However, we need to assume some value and can hope that + a reasonably strong algorithm has been chosen for our hypothetical + high-security task.) + + If the adversary can command a highly parallel processor or a large + network of work stations, 10^11 cycles per second is probably a + minimum assumption today. Looking forward a few years, there should + be at least an order of magnitude improvement. Thus, it is + reasonable to assume that 10^10 keys could be checked per second, or + 3.6*10^12 per hour or 6*10^14 per week, or 2.4*10^15 per month. This + implies a need for a minimum of 63 bits of randomness in keys, to be + sure that they cannot be found in a month. Even then it is possible + that, a few years from now, a highly determined and resourceful + adversary could break the key in 2 weeks; on average, they need try + only half the keys. + + These questions are considered in detail in "Minimal Key Lengths for + Symmetric Ciphers to Provide Adequate Commercial Security: A Report + by an Ad Hoc Group of Cryptographers and Computer Scientists" + [KeyStudy] that was sponsored by the Business Software Alliance. It + concluded that a reasonable key length in 1995 for very high security + is in the range of 75 to 90 bits and, since the cost of cryptography + does not vary much with the key size, it recommends 90 bits. To + update these recommendations, just add 2/3 of a bit per year for + Moore's law [MOORE]. This translates to a determination, in the year + 2004, a reasonable key length is in the 81- to 96-bit range. In + fact, today, it is increasingly common to use keys longer than 96 + + + + +Eastlake, et al. Standards Track [Page 36] + +RFC 4086 Randomness Requirements for Security June 2005 + + + bits, such as 128-bit (or longer) keys with AES and keys with + effective lengths of 112-bits with triple-DES. + +8.2.2. Meet-in-the-Middle Attacks + + If chosen or known plain text and the resulting encrypted text are + available, a "meet-in-the-middle" attack is possible if the structure + of the encryption algorithm allows it. (In a known plain text + attack, the adversary knows all or part (possibly some standard + header or trailer fields) of the messages being encrypted. In a + chosen plain text attack, the adversary can force some chosen plain + text to be encrypted, possibly by "leaking" an exciting text that is + sent by the adversary over an encrypted channel because the text is + so interesting. + + The following is an oversimplified explanation of the meet-in-the- + middle attack: the adversary can half-encrypt the known or chosen + plain text with all possible first half-keys, sort the output, and + then half-decrypt the encoded text with all the second half-keys. If + a match is found, the full key can be assembled from the halves and + used to decrypt other parts of the message or other messages. At its + best, this type of attack can halve the exponent of the work required + by the adversary while adding a very large but roughly constant + factor of effort. Thus, if this attack can be mounted, a doubling of + the amount of randomness in the very strong key to a minimum of 192 + bits (96*2) is required for the year 2004, based on the [KeyStudy] + analysis. + + This amount of randomness is well beyond the limit of that in the + inputs recommended by the US DoD for password generation and could + require user-typing timing, hardware random number generation, or + other sources of randomness. + + The meet-in-the-middle attack assumes that the cryptographic + algorithm can be decomposed in this way. Hopefully no modern + algorithm has this weakness, but there may be cases where we are not + sure of that or even of what algorithm a key will be used with. Even + if a basic algorithm is not subject to a meet-in-the-middle attack, + an attempt to produce a stronger algorithm by applying the basic + algorithm twice (or two different algorithms sequentially) with + different keys will gain less added security than would be expected. + Such a composite algorithm would be subject to a meet-in-the-middle + attack. + + Enormous resources may be required to mount a meet-in-the-middle + attack, but they are probably within the range of the national + security services of a major nation. Essentially all nations spy on + other nations' traffic. + + + +Eastlake, et al. Standards Track [Page 37] + +RFC 4086 Randomness Requirements for Security June 2005 + + +8.2.3. Other Considerations + + [KeyStudy] also considers the possibilities of special-purpose code- + breaking hardware and having an adequate safety margin. + + Note that key length calculations such as those above are + controversial and depend on various assumptions about the + cryptographic algorithms in use. In some cases, a professional with + a deep knowledge of algorithm-breaking techniques and of the strength + of the algorithm in use could be satisfied with less than half of the + 192 bit key size derived above. + + For further examples of conservative design principles, see + [FERGUSON]. + +9. Conclusion + + Generation of unguessable "random" secret quantities for security use + is an essential but difficult task. + + Hardware techniques for producing the needed entropy would be + relatively simple. In particular, the volume and quality would not + need to be high, and existing computer hardware, such as audio input + or disk drives, can be used. + + Widely-available computational techniques can process low-quality + random quantities from multiple sources, or a larger quantity of such + low-quality input from one source, to produce a smaller quantity of + higher-quality keying material. In the absence of hardware sources + of randomness, a variety of user and software sources can frequently, + with care, be used instead. However, most modern systems already + have hardware, such as disk drives or audio input, that could be used + to produce high-quality randomness. + + Once a sufficient quantity of high-quality seed key material (a + couple of hundred bits) is available, computational techniques are + available to produce cryptographically-strong sequences of + computationally-unpredictable quantities from this seed material. + +10. Security Considerations + + The entirety of this document concerns techniques and recommendations + for generating unguessable "random" quantities for use as passwords, + cryptographic keys, initialization vectors, sequence numbers, and + similar security applications. + + + + + + +Eastlake, et al. Standards Track [Page 38] + +RFC 4086 Randomness Requirements for Security June 2005 + + +11. Acknowledgements + + Special thanks to Paul Hoffman and John Kelsey for their extensive + comments and to Peter Gutmann, who has permitted the incorporation of + material from his paper "Software Generation of Practically Strong + Random Numbers". + + The following people (in alphabetic order) have contributed + substantially to this document: + + Steve Bellovin, Daniel Brown, Don Davis, Peter Gutmann, Tony + Hansen, Sandy Harris, Paul Hoffman, Scott Hollenback, Russ + Housley, Christian Huitema, John Kelsey, Mats Naslund, and Damir + Rajnovic. + + The following people (in alphabetic order) contributed to RFC 1750, + the predecessor of this document: + + David M. Balenson, Don T. Davis, Carl Ellison, Marc Horowitz, + Christian Huitema, Charlie Kaufman, Steve Kent, Hal Murray, Neil + Haller, Richard Pitkin, Tim Redmond, and Doug Tygar. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Eastlake, et al. Standards Track [Page 39] + +RFC 4086 Randomness Requirements for Security June 2005 + + +Appendix A: Changes from RFC 1750 + + 1. Additional acknowledgements have been added. + + 2. Insertion of section 5.3 on mixing with S-boxes. + + 3. Addition of section 3.3 on Ring Oscillator randomness sources. + + 4. Addition of AES and the members of the SHA series producing more + than 160 bits. Use of AES has been emphasized and the use of DES + de-emphasized. + + 5. Addition of section 6.3 on entropy pool techniques. + + 6. Addition of section 7.2.3 on the pseudo-random number generation + techniques given in FIPS 186-2 (with Change Notice 1), 7.2.1 on + those given in X9.82, section 7.1.2 on the random number + generation techniques of the /dev/random device in Linux and other + UNIX systems, and section 7.1.3 on random number generation + techniques in the Windows operating system. + + 7. Addition of references to the "Minimal Key Lengths for Symmetric + Ciphers to Provide Adequate Commercial Security" study published + in January 1996 [KeyStudy] and to [RFC1948]. + + 8. Added caveats to using Diffie-Hellman as a mixing function and, + because of those caveats and its computationally intensive nature, + recommend against its use. + + 9. Addition of references to the X9.82 effort and the [TURBID] and + [NASLUND] papers. + + 10. Addition of discussion of min-entropy and Renyi entropy and + references to the [LUBY] book. + + 11. Major restructuring, minor wording changes, and a variety of + reference updates. + + + + + + + + + + + + + + +Eastlake, et al. Standards Track [Page 40] + +RFC 4086 Randomness Requirements for Security June 2005 + + +Informative References + + [AES] "Specification of the Advanced Encryption Standard + (AES)", United States of America, US National + Institute of Standards and Technology, FIPS 197, + November 2001. + + [ASYMMETRIC] Simmons, G., Ed., "Secure Communications and + Asymmetric Cryptosystems", AAAS Selected Symposium + 69, ISBN 0-86531-338-5, Westview Press, 1982. + + [BBS] Blum, L., Blum, M., and M. Shub, "A Simple + Unpredictable Pseudo-Random Number Generator", SIAM + Journal on Computing, v. 15, n. 2, 1986. + + [BRILLINGER] Brillinger, D., "Time Series: Data Analysis and + Theory", Holden-Day, 1981. + + [CRC] "C.R.C. Standard Mathematical Tables", Chemical + Rubber Publishing Company. + + [DAVIS] Davis, D., Ihaka, R., and P. Fenstermacher, + "Cryptographic Randomness from Air Turbulence in Disk + Drives", Advances in Cryptology - Crypto '94, + Springer-Verlag Lecture Notes in Computer Science + #839, 1984. + + [DES] "Data Encryption Standard", US National Institute of + Standards and Technology, FIPS 46-3, October 1999. + Also, "Data Encryption Algorithm", American National + Standards Institute, ANSI X3.92-1981. See also FIPS + 112, "Password Usage", which includes FORTRAN code + for performing DES. + + [D-H] Rescorla, E., "Diffie-Hellman Key Agreement Method", + RFC 2631, June 1999. + + [DNSSEC1] Arends, R., Austein, R., Larson, M., Massey, D., and + S. Rose, "DNS Security Introduction and + Requirements", RFC 4033, March 2005. + + [DNSSEC2] Arends, R., Austein, R., Larson, M., Massey, D., and + S. Rose, "Resource Records for the DNS Security + Extensions", RFC 4034, March 2005. + + [DNSSEC3] Arends, R., Austein, R., Larson, M., Massey, D., and + S. Rose, "Protocol Modifications for the DNS Security + Extensions", RFC 4035, March 2005. + + + +Eastlake, et al. Standards Track [Page 41] + +RFC 4086 Randomness Requirements for Security June 2005 + + + [DoD] "Password Management Guideline", United States of + America, Department of Defense, Computer Security + Center, CSC-STD-002-85, April 1885. + + (See also "Password Usage", FIPS 112, which + incorporates CSC-STD-002-85 as one of its appendices. + FIPS 112 is currently available at: + http://www.idl.nist.gov/fipspubs/fip112.htm.) + + [DSS] "Digital Signature Standard (DSS)", US National + Institute of Standards and Technology, FIPS 186-2, + January 2000. + + [DSS_CN1] "Digital Signature Standard Change Notice 1", US + National Institute of Standards and Technology, FIPS + 186-2 Change Notice 1, 5, October 2001. + + [FERGUSON] Ferguson, N. and B. Schneier, "Practical + Cryptography", Wiley Publishing Inc., ISBN + 047122894X, April 2003. + + [GIFFORD] Gifford, D., "Natural Random Number", MIT/LCS/TM-371, + September 1988. + + [IEEE_802.11i] "Amendment to Standard for Telecommunications and + Information Exchange Between Systems - LAN/MAN + Specific Requirements - Part 11: Wireless Medium + Access Control (MAC) and physical layer (PHY) + specifications: Medium Access Control (MAC) Security + Enhancements", IEEE, January 2004. + + [IPSEC] Kent, S. and R. Atkinson, "Security Architecture for + the Internet Protocol", RFC 2401, November 1998. + + [Jakobsson] Jakobsson, M., Shriver, E., Hillyer, B., and A. + Juels, "A practical secure random bit generator", + Proceedings of the Fifth ACM Conference on Computer + and Communications Security, 1998. + + [KAUFMAN] Kaufman, C., Perlman, R., and M. Speciner, "Network + Security: Private Communication in a Public World", + Prentis Hall PTR, ISBN 0-13-046019-2, 2nd Edition + 2002. + + + + + + + + +Eastlake, et al. Standards Track [Page 42] + +RFC 4086 Randomness Requirements for Security June 2005 + + + [KeyStudy] Blaze, M., Diffie, W., Riverst, R., Schneier, B. + Shimomura, T., Thompson, E., and M. Weiner, "Minimal + Key Lengths for Symmetric Ciphers to Provide Adequate + Commercial Security: A Report by an Ad Hoc Group of + Cryptographers and Computer Scientists", January + 1996. Currently available at: + http://www.crypto.com/papers/keylength.txt and + http://www.securitydocs.com/library/441. + + [KNUTH] Knuth, D., "The Art of Computer Programming", Volume + 2: Seminumerical Algorithms, Chapter 3: Random + Numbers, Addison-Wesley Publishing Company, 3rd + Edition, November 1997. + + [KRAWCZYK] Krawczyk, H., "How to Predict Congruential + Generators", Journal of Algorithms, V. 13, N. 4, + December 1992. + + [LUBY] Luby, M., "Pseudorandomness and Cryptographic + Applications", Princeton University Press, ISBN + 0691025460, 8 January 1996. + + [MAIL_PEM1] Linn, J., "Privacy Enhancement for Internet + Electronic Mail: Part I: Message Encryption and + Authentication Procedures", RFC 1421, February 1993. + + [MAIL_PEM2] Kent, S., "Privacy Enhancement for Internet + Electronic Mail: Part II: Certificate-Based Key + Management", RFC 1422, February 1993. + + [MAIL_PEM3] Balenson, D., "Privacy Enhancement for Internet + Electronic Mail: Part III: Algorithms, Modes, and + Identifiers", RFC 1423, February 1993. + + [MAIL_PEM4] Kaliski, B., "Privacy Enhancement for Internet + Electronic Mail: Part IV: Key Certification and + Related Services", RFC 1424, February 1993. + + [MAIL_PGP1] Callas, J., Donnerhacke, L., Finney, H., and R. + Thayer, "OpenPGP Message Format", RFC 2440, November + 1998. + + [MAIL_PGP2] Elkins, M., Del Torto, D., Levien, R., and T. + Roessler, "MIME Security with OpenPGP", RFC 3156, + August 2001. + + + + + + +Eastlake, et al. Standards Track [Page 43] + +RFC 4086 Randomness Requirements for Security June 2005 + + + [S/MIME] RFCs 2632 through 2634: + + Ramsdell, B., "S/MIME Version 3 Certificate + Handling", RFC 2632, June 1999. + + Ramsdell, B., "S/MIME Version 3 Message + Specification", RFC 2633, June 1999. + + Hoffman, P., "Enhanced Security Services for S/MIME", + RFC 2634, June 1999. + + [MD4] Rivest, R., "The MD4 Message-Digest Algorithm", RFC + 1320, April 1992. + + [MD5] Rivest, R., "The MD5 Message-Digest Algorithm ", RFC + 1321, April 1992. + + [MODES] "DES Modes of Operation", US National Institute of + Standards and Technology, FIPS 81, December 1980. + Also: "Data Encryption Algorithm - Modes of + Operation", American National Standards Institute, + ANSI X3.106-1983. + + [MOORE] Moore's Law: the exponential increase in the logic + density of silicon circuits. Originally formulated + by Gordon Moore in 1964 as a doubling every year + starting in 1962, in the late 1970s the rate fell to + a doubling every 18 months and has remained there + through the date of this document. See "The New + Hacker's Dictionary", Third Edition, MIT Press, ISBN + 0-262-18178-9, Eric S. Raymond, 1996. + + [NASLUND] Naslund, M. and A. Russell, "Extraction of Optimally + Unbiased Bits from a Biased Source", IEEE + Transactions on Information Theory. 46(3), May 2000. + + [ORMAN] Orman, H. and P. Hoffman, "Determining Strengths For + Public Keys Used For Exchanging Symmetric Keys", BCP + 86, RFC 3766, April 2004. + + [RFC1750] Eastlake 3rd, D., Crocker, S., and J. Schiller, + "Randomness Recommendations for Security", RFC 1750, + December 1994. + + [RFC1948] Bellovin, S., "Defending Against Sequence Number + Attacks", RFC 1948, May 1996. + + + + + +Eastlake, et al. Standards Track [Page 44] + +RFC 4086 Randomness Requirements for Security June 2005 + + + [RFC2104] Krawczyk, H., Bellare, M., and R. Canetti, "HMAC: + Keyed-Hashing for Message Authentication", RFC 2104, + February 1997. + + [RSA_BULL1] "Suggestions for Random Number Generation in + Software", RSA Laboratories Bulletin #1, January + 1996. + + [RSA_BULL13] Silverman, R., "A Cost-Based Security Analysis of + Symmetric and Asymmetric Key Lengths", RSA + Laboratories Bulletin #13, April 2000 (revised + November 2001). + + [SBOX1] Mister, S. and C. Adams, "Practical S-box Design", + Selected Areas in Cryptography, 1996. + + [SBOX2] Nyberg, K., "Perfect Non-linear S-boxes", Advances in + Cryptography, Eurocrypt '91 Proceedings, Springer- + Verland, 1991. + + [SCHNEIER] Schneier, B., "Applied Cryptography: Protocols, + Algorithms, and Source Code in C", 2nd Edition, John + Wiley & Sons, 1996. + + [SHANNON] Shannon, C., "The Mathematical Theory of + Communication", University of Illinois Press, 1963. + Originally from: Bell System Technical Journal, July + and October, 1948. + + [SHIFT1] Golub, S., "Shift Register Sequences", Aegean Park + Press, Revised Edition, 1982. + + [SHIFT2] Barker, W., "Cryptanalysis of Shift-Register + Generated Stream Cypher Systems", Aegean Park Press, + 1984. + + [SHA] "Secure Hash Standard", US National Institute of + Science and Technology, FIPS 180-2, 1 August 2002. + + [SHA_RFC] Eastlake 3rd, D. and P. Jones, "US Secure Hash + Algorithm 1 (SHA1)", RFC 3174, September 2001. + + [SSH] Products of the SECSH Working Group, Works in + Progress, 2005. + + [STERN] Stern, J., "Secret Linear Congruential Generators are + not Cryptographically Secure", Proc. IEEE STOC, 1987. + + + + +Eastlake, et al. Standards Track [Page 45] + +RFC 4086 Randomness Requirements for Security June 2005 + + + [TLS] Dierks, T. and C. Allen, "The TLS Protocol Version + 1.0", RFC 2246, January 1999. + + [TURBID] Denker, J., "High Entropy Symbol Generator", + <http://www.av8n.com/turbid/paper/turbid.htm>, 2003. + + [USENET_1] Kantor, B. and P. Lapsley, "Network News Transfer + Protocol", RFC 977, February 1986. + + [USENET_2] Barber, S., "Common NNTP Extensions", RFC 2980, + October 2000. + + [VON_NEUMANN] Von Nuemann, J., "Various techniques used in + connection with random digits", Von Neumann's + Collected Works, Vol. 5, Pergamon Press, 1963. + + [WSC] Howard, M. and D. LeBlanc, "Writing Secure Code, + Second Edition", Microsoft Press, ISBN 0735617228, + December 2002. + + [X9.17] "American National Standard for Financial Institution + Key Management (Wholesale)", American Bankers + Association, 1985. + + [X9.82] "Random Number Generation", American National + Standards Institute, ANSI X9F1, Work in Progress. + Part 1 - Overview and General Principles. + Part 2 - Non-Deterministic Random Bit Generators + Part 3 - Deterministic Random Bit Generators + + + + + + + + + + + + + + + + + + + + + + +Eastlake, et al. Standards Track [Page 46] + +RFC 4086 Randomness Requirements for Security June 2005 + + +Authors' Addresses + + Donald E. Eastlake 3rd + Motorola Laboratories + 155 Beaver Street + Milford, MA 01757 USA + + Phone: +1 508-786-7554 (w) + +1 508-634-2066 (h) + EMail: Donald.Eastlake@motorola.com + + + Jeffrey I. Schiller + MIT, Room E40-311 + 77 Massachusetts Avenue + Cambridge, MA 02139-4307 USA + + Phone: +1 617-253-0161 + EMail: jis@mit.edu + + + Steve Crocker + + EMail: steve@stevecrocker.com + + + + + + + + + + + + + + + + + + + + + + + + + + + +Eastlake, et al. Standards Track [Page 47] + +RFC 4086 Randomness Requirements for Security June 2005 + + +Full Copyright Statement + + Copyright (C) The Internet Society (2005). + + This document is subject to the rights, licenses and restrictions + contained in BCP 78, and except as set forth therein, the authors + retain all their rights. + + This document and the information contained herein are provided on an + "AS IS" basis and THE CONTRIBUTOR, THE ORGANIZATION HE/SHE REPRESENTS + OR IS SPONSORED BY (IF ANY), THE INTERNET SOCIETY AND THE INTERNET + ENGINEERING TASK FORCE DISCLAIM ALL WARRANTIES, EXPRESS OR IMPLIED, + INCLUDING BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE + INFORMATION HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED + WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + +Intellectual Property + + The IETF takes no position regarding the validity or scope of any + Intellectual Property Rights or other rights that might be claimed to + pertain to the implementation or use of the technology described in + this document or the extent to which any license under such rights + might or might not be available; nor does it represent that it has + made any independent effort to identify any such rights. Information + on the procedures with respect to rights in RFC documents can be + found in BCP 78 and BCP 79. + + Copies of IPR disclosures made to the IETF Secretariat and any + assurances of licenses to be made available, or the result of an + attempt made to obtain a general license or permission for the use of + such proprietary rights by implementers or users of this + specification can be obtained from the IETF on-line IPR repository at + http://www.ietf.org/ipr. + + The IETF invites any interested party to bring to its attention any + copyrights, patents or patent applications, or other proprietary + rights that may cover technology that may be required to implement + this standard. Please address the information to the IETF at ietf- + ipr@ietf.org. + +Acknowledgement + + Funding for the RFC Editor function is currently provided by the + Internet Society. + + + + + + + +Eastlake, et al. Standards Track [Page 48] + diff --git a/src/common/libManageSieve/doc/IMAP4 Referrals/rfc2221.txt b/src/common/libManageSieve/doc/IMAP4 Referrals/rfc2221.txt new file mode 100644 index 00000000..78f73c90 --- /dev/null +++ b/src/common/libManageSieve/doc/IMAP4 Referrals/rfc2221.txt @@ -0,0 +1,284 @@ + + + + + +Network Working Group M. Gahrns +Request for Comments: 2221 Microsoft +Category: Standards Track October 1997 + + + IMAP4 Login Referrals + +Status of this Memo + + This document specifies an Internet standards track protocol for the + Internet community, and requests discussion and suggestions for + improvements. Please refer to the current edition of the "Internet + Official Protocol Standards" (STD 1) for the standardization state + and status of this protocol. Distribution of this memo is unlimited. + +Copyright Notice + + Copyright (C) The Internet Society (1997). All Rights Reserved. + +1. Abstract + + When dealing with large amounts of users and many IMAP4 [RFC-2060] + servers, it is often necessary to move users from one IMAP4 server to + another. For example, hardware failures or organizational changes + may dictate such a move. + + Login referrals allow clients to transparently connect to an + alternate IMAP4 server, if their home IMAP4 server has changed. + + A referral mechanism can provide efficiencies over the alternative + 'proxy method', in which the local IMAP4 server contacts the remote + server on behalf of the client, and then transfers the data from the + remote server to itself, and then on to the client. The referral + mechanism's direct client connection to the remote server is often a + more efficient use of bandwidth, and does not require the local + server to impersonate the client when authenticating to the remote + server. + +2. Conventions used in this document + + In examples, "C:" and "S:" indicate lines sent by the client and + server respectively. + + A home server, is an IMAP4 server that contains the user's inbox. + + A remote server is a server that contains remote mailboxes. + + + + + +Gahrns Standards Track [Page 1] + +RFC 2221 IMAP4 Login Referrals October 1997 + + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this + document are to be interpreted as described in [RFC-2119]. + +3. Introduction and Overview + + IMAP4 servers that support this extension MUST list the keyword + LOGIN-REFERRALS in their CAPABILITY response. No client action is + needed to invoke the LOGIN-REFERRALS capability in a server. + + A LOGIN-REFERRALS capable IMAP4 server SHOULD NOT return a referral + to a server that will return a referral. A client MUST NOT follow + more than 10 levels of referral without consulting the user. + + A LOGIN-REFERRALS response code MUST contain as an argument a valid + IMAP server URL as defined in [IMAP-URL]. + + A home server referral consists of either a tagged NO or OK, or an + untagged BYE response that contains a LOGIN-REFERRALS response code. + + Example: A001 NO [REFERRAL IMAP://user;AUTH=*@SERVER2/] Remote Server + + NOTE: user;AUTH=* is specified as required by [IMAP-URL] to avoid a + client falling back to anonymous login. + +4. Home Server Referrals + + A home server referral may be returned in response to an AUTHENTICATE + or LOGIN command, or it may appear in the connection startup banner. + If a server returns a home server referral in a tagged NO response, + that server does not contain any mailboxes that are accessible to the + user. If a server returns a home server referral in a tagged OK + response, it indicates that the user's personal mailboxes are + elsewhere, but the server contains public mailboxes which are + readable by the user. After receiving a home server referral, the + client can not make any assumptions as to whether this was a + permanent or temporary move of the user. + +4.1. LOGIN and AUTHENTICATE Referrals + + An IMAP4 server MAY respond to a LOGIN or AUTHENTICATE command with a + home server referral if it wishes to direct the user to another IMAP4 + server. + + Example: C: A001 LOGIN MIKE PASSWORD + S: A001 NO [REFERRAL IMAP://MIKE@SERVER2/] Specified user + is invalid on this server. Try SERVER2. + + + + +Gahrns Standards Track [Page 2] + +RFC 2221 IMAP4 Login Referrals October 1997 + + + Example: C: A001 LOGIN MATTHEW PASSWORD + S: A001 OK [REFERRAL IMAP://MATTHEW@SERVER2/] Specified + user's personal mailboxes located on Server2, but + public mailboxes are available. + + Example: C: A001 AUTHENTICATE GSSAPI + <authentication exchange> + S: A001 NO [REFERRAL IMAP://user;AUTH=GSSAPI@SERVER2/] + Specified user is invalid on this server. Try + SERVER2. + +4.2. BYE at connection startup referral + + An IMAP4 server MAY respond with an untagged BYE and a REFERRAL + response code that contains an IMAP URL to a home server if it is not + willing to accept connections and wishes to direct the client to + another IMAP4 server. + + Example: S: * BYE [REFERRAL IMAP://user;AUTH=*@SERVER2/] Server not + accepting connections. Try SERVER2 + +5. Formal Syntax + + The following syntax specification uses the augmented Backus-Naur + Form (BNF) as described in [ABNF]. + + This amends the "resp_text_code" element of the IMAP4 grammar + described in [RFC-2060] + + resp_text_code =/ "REFERRAL" SPACE <imapurl> + ; See [IMAP-URL] for definition of <imapurl> + ; See [RFC-2060] for base definition of resp_text_code + +6. Security Considerations + + The IMAP4 login referral mechanism makes use of IMAP URLs, and as + such, have the same security considerations as general internet URLs + [RFC-1738], and in particular IMAP URLs [IMAP-URL]. + + A server MUST NOT give a login referral if authentication for that + user fails. This is to avoid revealing information about the user's + account to an unauthorized user. + + With the LOGIN-REFERRALS capability, it is potentially easier to + write a rogue 'password catching' server that collects login data and + then refers the client to their actual IMAP4 server. Although + referrals reduce the effort to write such a server, the referral + response makes detection of the intrusion easier. + + + +Gahrns Standards Track [Page 3] + +RFC 2221 IMAP4 Login Referrals October 1997 + + +7. References + + [RFC-2060], Crispin, M., "Internet Message Access Protocol - Version + 4rev1", RFC 2060, December 1996. + + [IMAP-URL], Newman, C., "IMAP URL Scheme", RFC 2192, Innosoft, + September 1997. + + [RFC-1738], Berners-Lee, T., Masinter, L. and M. McCahill, "Uniform + Resource Locators (URL)", RFC 1738, December 1994. + + [RFC-2119], Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", RFC 2119, March 1997. + + [ABNF], DRUMS working group, Dave Crocker Editor, "Augmented BNF for + Syntax Specifications: ABNF", Work in Progress. + +8. Acknowledgments + + Many valuable suggestions were received from private discussions and + the IMAP4 mailing list. In particular, Raymond Cheng, Mark Crispin, + Mark Keasling Chris Newman and Larry Osterman made significant + contributions to this document. + +9. Author's Address + + Mike Gahrns + Microsoft + One Microsoft Way + Redmond, WA, 98072 + + Phone: (206) 936-9833 + EMail: mikega@microsoft.com + + + + + + + + + + + + + + + + + + +Gahrns Standards Track [Page 4] + +RFC 2221 IMAP4 Login Referrals October 1997 + + +10. Full Copyright Statement + + Copyright (C) The Internet Society (1997). All Rights Reserved. + + This document and translations of it may be copied and furnished to + others, and derivative works that comment on or otherwise explain it + or assist in its implmentation may be prepared, copied, published + andand distributed, in whole or in part, without restriction of any + kind, provided that the above copyright notice and this paragraph are + included on all such copies and derivative works. However, this + document itself may not be modified in any way, such as by removing + the copyright notice or references to the Internet Society or other + Internet organizations, except as needed for the purpose of + developing Internet standards in which case the procedures for + copyrights defined in the Internet Standards process must be + followed, or as required to translate it into languages other than + English. + + The limited permissions granted above are perpetual and will not be + revoked by the Internet Society or its successors or assigns. + + This document and the information contained herein is provided on an + "AS IS" basis and THE INTERNET SOCIETY AND THE INTERNET ENGINEERING + TASK FORCE DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING + BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION + HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED WARRANTIES OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE." + + + + + + + + + + + + + + + + + + + + + + + + +Gahrns Standards Track [Page 5] + + + diff --git a/src/common/libManageSieve/doc/Manage Sieve/draft-martin-managesieve-06.txt b/src/common/libManageSieve/doc/Manage Sieve/draft-martin-managesieve-06.txt new file mode 100644 index 00000000..6deab9c1 --- /dev/null +++ b/src/common/libManageSieve/doc/Manage Sieve/draft-martin-managesieve-06.txt @@ -0,0 +1,1237 @@ +Network Working Group Tim Martin +Document: draft-martin-managesieve-06.txt Mirapoint Inc. +Expires: August 2006 Alexey Melnikov + Isode Limited + February 2006 + + + A Protocol for Remotely Managing Sieve Scripts + + <draft-martin-managesieve-06.txt> + +Status of this Memo + + By submitting this Internet-Draft, each author represents that any + applicable patent or other IPR claims of which he or she is aware + have been or will be disclosed, and any of which he or she becomes + aware will be disclosed, in accordance with Section 6 of BCP 79. + + Internet-Drafts are working documents of the Internet Engineering + Task Force (IETF), its areas, and its working groups. Note that + other groups may also distribute working documents as Internet- + Drafts. + + Internet-Drafts are draft documents valid for a maximum of six months + and may be updated, replaced, or obsoleted by other documents at any + time. It is inappropriate to use Internet-Drafts as reference + material or to cite them other than as "work in progress." + + The list of current Internet-Drafts can be accessed at + http://www.ietf.org/ietf/1id-abstracts.txt. + + The list of Internet-Draft Shadow Directories can be accessed at + http://www.ietf.org/shadow.html. + + Distribution of this memo is unlimited. + +Copyright Notice + + Copyright (C) The Internet Society (2006). + + +Abstract + + Sieve scripts allow users to filter incoming email. Message stores + are commonly sealed servers so users cannot log into them, yet users + must be able to update their scripts on them. This document + describes a protocol "sieve" for securely managing Sieve scripts on + a remote server. This protocol allows a user to have multiple + scripts, and also alerts a user to syntactically flawed scripts. + + + Table of Contents + + + +Status of this Memo . . . . . . . . . . . . . . . . . . . . . . . . 1 +Abstract . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 +1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 3 +1.1. Changes . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 +1.2. Conventions Used in the Document . . . . . . . . . . . . . . 4 +1.3. Syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 +1.4. Response Codes . . . . . . . . . . . . . . . . . . . . . . . 5 +1.5. Active Script . . . . . . . . . . . . . . . . . . . . . . . . 6 +1.6. Quotas . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 +1.7. Script Names . . . . . . . . . . . . . . . . . . . . . . . . 7 +1.8. Capabilities . . . . . . . . . . . . . . . . . . . . . . . . 7 +2. Commands . . . . . . . . . . . . . . . . . . . . . . . . . . 8 +2.1. AUTHENTICATE Command . . . . . . . . . . . . . . . . . . . . 8 +2.2. STARTTLS Command . . . . . . . . . . . . . . . . . . . . . . 10 +2.3. LOGOUT Command . . . . . . . . . . . . . . . . . . . . . . . 10 +2.4. CAPABILITY Command . . . . . . . . . . . . . . . . . . . . . 11 +2.5. HAVESPACE Command . . . . . . . . . . . . . . . . . . . . . . 11 +2.6. PUTSCRIPT Command . . . . . . . . . . . . . . . . . . . . . . 11 +2.7. LISTSCRIPTS Command . . . . . . . . . . . . . . . . . . . . . 13 +2.8. SETACTIVE Command . . . . . . . . . . . . . . . . . . . . . . 13 +2.9. GETSCRIPT Command . . . . . . . . . . . . . . . . . . . . . . 13 +2.10. DELETESCRIPT Command . . . . . . . . . . . . . . . . . . . . 14 +3. Sieve URL Scheme . . . . . . . . . . . . . . . . . . . . . . 14 +4. Formal Syntax . . . . . . . . . . . . . . . . . . . . . . . . 15 +5. Security Considerations . . . . . . . . . . . . . . . . . . . 18 +7. References . . . . . . . . . . . . . . . . . . . . . . . . . +7.1. Normative References . . . . . . . . . . . . . . . . . . . . +7.2. Informative References . . . . . . . . . . . . . . . . . . . +8. Author's Address . . . . . . . . . . . . . . . . . . . . . . + + +1. Introduction + + + +1.1. Changes + + [[Note to RFC editor: please delete this section before publication]] + + Changes since 05 + + -More ABNF fixes + + -Added IANA considerations + + -Added/fixed text about AUTHENTICATE. + + -Updated the text om Sieve URLs. + + -Updated and added new examples. + + Changes since 04 + + -Updated boilerplate and some references. Added Alexey as co-editor. + + -Minor ABNF fixes + + -Cleaned up terminology (for example, made more consistent with SASL) + + -Added more examples, fixed some existing examples + + -Clarified that STARTTLS command is optional + + -Clarified that disabling an active script when there is no script active + is not an error. + + Changes since 03 + + -Add referals and Sieve URLs + + -Lots of spelling/grammer fixes + + -Don't give capabilities after successful STARTTLS. This is because + it isn't consistant with AUTHENTICATE. There is language specifying + that a client should re-issue a CAPABILITY command after + AUTHENTICATE/STARTTLS. + + -Putting a script of length 0 doesn't remove the script. If this + functionality is desired, the DELETESCRIPT command should be used. + + Changes since 02 + + -add BYE response + + -typo on line 588 + + -allow ANONYMOUS access for sieve script verification + + -updated SIEVE spec reference + + Changes since 01 + + -changed contact info + + Changes since 00 + + -added response codes (from ACAP) + + -removed special-ok response from authenticate command (response + codes obsolete it) + + -changed service name to "sieve" + + -ABNF fixes + + -Alexey's wording changes + + -Eliminated lame PLAIN paragraph + + Changes since PRE + + -dropped synchronized literals. added HAVESPACE command + + -changed capability response syntax. added CAPABILITY command + + -allowed pipelining + + - "sieve" -> "Sieve". Other minor fixes + + -made script names more flexible + + -added starttls support + + +1.2. Conventions Used in the Document + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this + document are to be interpreted as described in [KEYWORDS]. + + In examples, "C:" and "S:" indicate lines sent by the client and + server respectively. Line breaks that do not start a new "C:" or + "S:" exist for editorial reasons. + + +1.3. Syntax + + This a line oriented protocol much like [IMAP4rev1] or [ACAP]. There + are three types: ATOMS, numbers and strings. Strings may be quoted + or literal. See [ACAP] for detailed descriptions of these types. + + Each command consists of an atom followed by zero or more strings + and numbers terminated by a newline. + + All client queries are replied to with either an OK, NO, or BYE + response. Each response may be followed by a response code (see + response codes section) and by a string consisting of human readable + text in the local language. The contents of the string SHOULD be + shown to the user and implementations MUST NOT attempt to parse the + message for meaning. + + The BYE response may be used if the server wishes to close the + connection. A server may wish to do this because the client was idle + for too long or there were too many failed authentication attempts. This + response can be issued at any time and should be immediately followed + by a server hang-up of the connection. If a server has a inactivity + timeout resulting in client autologout it MUST be no less than 30 + minutes. + + <<IANA registration is pending. Current implementations generally use + port number 2000.>> + + +1.4. Response Codes + + An OK, NO, or BYE response from the server MAY contain a response + code to describe the event in a more detailed machine parsable + fashion. A response code consists of data inside parentheses in the + form of an atom, possibly followed by a space and arguments. + Response codes are defined when there is a specific action that a + client can take based upon the additional information. In order to + support future extension, the response code is represented as a + slash-separated hierarchy with each level of hierarchy representing + increasing detail about the error. Clients MUST tolerate additional + hierarchical response code detail which they don't understand. + + The currently defined response codes are: + + AUTH-TOO-WEAK + + This response code is returned in the NO response from an + AUTHENTICATE command. It indicates that site security policy forbids + the use of the requested mechanism for the specified authentication + identity. + + ENCRYPT-NEEDED + + This response code is returned on NO result from an AUTHENTICATE + command. It indicates that site security policy requires the use of + a strong encryption mechanism for the specified authentication + identity and mechanism. + + QUOTA + + The command would have placed the user above the site-defined quota + constraints. + + REFERRAL + + This response code may be returned with a BYE result from any + command, and includes a mandatory parameter that indicates what + server to access to manage this user's sieve scripts. The server + will be specified by a Sieve URL (see "Sieve URL Scheme" section). + The scriptname portion of the URL MUST NOT be specified. The client + should authenticate to the specified server and use it for all + further commands in the current session. + + SASL + + This response code can occur in the OK response to a successful + AUTHENTICATE command and includes the optional final server response + data from the server as specified by [SASL]. + + TRANSITION-NEEDED + + This response code occurs in a NO response of an AUTHENTICATE + command. It indicates that the user name is valid, but the entry in + the authentication database needs to be updated in order to permit + authentication with the specified mechanism. This is typically done + by establishing a secure channel using TLS, followed by authenticating + once using the [PLAIN] authentication mechanism. The selected + mechanism SHOULD then work for authentications in subsequent sessions. + + This condition can happen if a user has an entry in a system + authentication database such as Unix /etc/passwd, but does not have + credentials suitable for use by the specified mechanism. + + + TRYLATER + + A command failed due to a temporary server failure. The client MAY + continue using local information and try the command later. + + Client implementations MUST tolerate response codes that they do not + recognize. + + +1.5. Active Script + + A user may have multiple Sieve scripts on the server, yet only one + script may be used for filtering of incoming messages. This is the + active script. Users may have zero or one active scripts and MUST + use the SETACTIVE command described below for changing the active + script or disabling Sieve processing. For example, a user may have + an everyday script they normally use and a special script they use + when they go on vacation. Users can change which script is being + used without having to download and upload a script stored somewhere + else. + + +1.6. Quotas + + Servers SHOULD impose quotas to prevent malicious users from + overflowing available storage. If a command would place a user over + a quota setting, servers MUST reply with a NO response. Client + implementations MUST be able to handle commands failing because of + quota restrictions. + + +1.7. Script Names + + Sieve script names may contain any valid UTF-8 characters, but names + must be at least one octet long. Zero octets script name + has special meaning (see SETACTIVE command section). Servers MUST + allow names of up to 128 UTF-8 octets <<(do we really want to specify + length in UTF-8 octets, as opposed to Unicode characters?)>> + in length, and may allow longer + names. + + +1.8. Capabilities + + Server capabilities are sent by the server upon a client connection. + Clients may request the capabilities at a later time by issuing the + CAPABILITY command described later. The capabilities consist of a + series of lines each with one or two strings. The first string is + the name of the capability, which is case-insensitive. The second + optional string is the value associated with that capability. + Order of capabilities is arbitrary, but each capability name can + appear at most once. + + The following capabilities are defined in this document: + + IMPLEMENTATION - Name of implementation and version + + SASL - List of SASL mechanisms supported by the server, each + separated by a space + + SIEVE - List of space separated Sieve extensions supported + + STARTTLS - If TLS [TLS] is supported by this implementation + + A server implementation MUST return SIEVE and IMPLEMENTATION + capabilities. + + A client implementation MUST ignore any other capabilities given + that it does not understand. + + Example: + + S: "IMPLEMENTATION" "CMU Cyrus Sieved v001" + S: "SASL" "DIGEST-MD5 GSSAPI" + S: "SIEVE" "FILEINTO VACATION" + S: "STARTTLS" + S: OK + + <<Add RENAMESCRIPT>> + + +2. Commands + + The following commands are valid. Prior to successful authentication + only the AUTHENTICATE, CAPABILITY, STARTTLS, and LOGOUT commands are + valid. Servers MUST reject all other commands with a NO response. + Clients may pipeline commands (send more than one command at a time + without waiting for completion of the first command ). However, a + group of commands sent together MUST NOT have an AUTHENTICATE, + a STARTTLS or a HAVESPACE command anywhere but the last command in + the list. + + +2.1. AUTHENTICATE Command + + Arguments: + String - mechanism + String - initial data (optional) + + The AUTHENTICATE command indicates a SASL [SASL] authentication + mechanism to the server. If the server supports the requested + authentication mechanism, it performs an authentication protocol + exchange to identify and authenticate the user. Optionally, it also + negotiates a security layer for subsequent protocol interactions. + If the requested authentication mechanism is not supported, the + server rejects the AUTHENTICATE command by sending the NO response. + + The authentication protocol exchange consists of a series of server + challenges and client responses that are specific to the selected + authentication mechanism. A server challenge consists of a string + (quoted or literal) followed by a CRLF. The contents of the string is + a base-64 encoding of the SASL data. A client response consists of + a string (quoted or literal) with the base-64 encoding of the SASL + data followed by a CRLF. If the client wishes to cancel the + authentication exchange, it issues a string containing a single "*". + If the server receives such a response, it MUST reject the + AUTHENTICATE command by sending an NO reply. + + Note that an empty challenge/response is sent as an empty string. + If the mechanism dictates that the final response is sent by the + server this data MAY be placed within the data portion of the SASL + response code to save a round trip. + + The optional initial-response argument to the AUTHENTICATE command + is used to save a round trip when using authentication mechanisms + that are defined to send no data in the initial challenge. When the + initial-response argument is used with such a mechanism, the initial + empty challenge is not sent to the client and the server uses the + data in the initial-response argument as if it were sent in response + to the empty challenge. If the initial-response argument to the + AUTHENTICATE command is used with a mechanism that sends data in the + initial challenge, the server rejects the AUTHENTICATE command by + sending the NO response. + + The service name specified by this protocol's profile of SASL is + "sieve". + + Reauthentication is not supported by ManageSieve protocol's profile + of SASL. I.e. after a successfully completed AUTHENTICATE command, + no more AUTHENTICATE commands may be issued in the same session. + After a successful AUTHENTICATE command completes, a server MUST + reject any further AUTHENTICATE commands with a NO reply. + + If a security layer is negotiated through the SASL authentication + exchange, it takes effect immediately following the CRLF that + concludes the authentication exchange for the client, and the CRLF + of the OK response for the server. + + When a security layer takes effect, the ManageSieve protocol is reset + to the initial state (the state in ManageSieve after a client + has connected to the server). The server MUST discard any + knowledge obtained from the client which was not obtained from + the SASL (or TLS) negotiation itself. + Likewise, the client MUST discard any knowledge obtained from + the server, such as the list of ManageSieve extensions, which + was not obtained from the SASL (or TLS) negotiation itself. + (Note that a client MAY compare the advertised SASL mechanisms before and + after authentication in order to detect an active down-negotiation attack. + See below.) + + Once a SASL security layer is established, the server MUST re-issue the + capability results, followed by an OK response. This is necessary to + protect against man-in-the-middle attacks which alter the capabilities + list prior to SASL negotiation. + The capability results MUST include all SASL mechanisms. This is done in + order to allow client to detect active down-negotiation attack. + + When both [TLS] and SASL security layers are in effect, the + TLS encoding MUST be applied (when sending data) after the SASL encoding, + regardless of the order in which the layers were negotiated. + + Server implementations SHOULD support SASL proxy authentication so + that an administrator can administer a user's scripts. Proxy + authentication is when a user authenticates as herself/himself but + requests the server to act (authorize) as another user. + + <<The authorization identity generated by this [SASL] exchange + is a simple username, and both client and server MUST use the + [SASLprep] profile of the [StringPrep] algorithm to prepare + these names for transmission or comparison. If preparation of + the authorization identity fails or results in an empty string + (unless it was transmitted as the empty string), the server + MUST fail the authentication.>> + + If an AUTHENTICATE command fails with a NO response, the client may + try another authentication mechanism by issuing another AUTHENTICATE + command. In other words, the client may request authentication + types in decreasing order of preference. + + Note that a failed NO response to the AUTHENTICATE command may contain + one of the following response codes: AUTH-TOO-WEAK, ENCRYPT-NEEDED or + TRANSITION-NEEDED. See section 1.4 for detailed description of the + relevant conditions. + + To ensure interoperability, client and server implementations + of this extension MUST implement the [DIGEST-MD5] SASL + mechanism. <<What is the IESG policy on this?>> + + + Implementations MAY advertise the ANONYMOUS SASL mechanism [SASL-ANON]. + This indicates that the server supports ANONYMOUS SIEVE + script syntax verification. Only the CAPABILITY, PUTSCRIPT and + LOGOUT commands are available to the anonymous user. All other + commands MUST give NO responses. Furthermore the PUTSCRIPT command + SHOULD NOT <<MUST NOT?>> store any data. In this mode a positive + response to the PUTSCRIPT command indicates that the given script + does not have any syntax errors. + + Examples (Note that long lines are folded for readability and are + not part of protocol exchange): + + S: "IMPLEMENTATION" "CMU Cyrus Sieved v001" + S: "SASL" "DIGEST-MD5 GSSAPI" + S: "SIEVE" "FILEINTO VACATION" + S: "STARTTLS" + S: OK + C: Authenticate "DIGEST-MD5" + S: "cmVhbG09ImVsd29vZC5pbm5vc29mdC5jb20iLG5vbmNlPSJPQTZNRzl0 + RVFHbTJoaCIscW9wPSJhdXRoIixhbGdvcml0aG09bWQ1LXNlc3MsY2hh + cnNldD11dGYtOA==" + C: "Y2hhcnNldD11dGYtOCx1c2VybmFtZT0iY2hyaXMiLHJlYWxtPSJlbHdvb2 + QuaW5ub3NvZnQuY29tIixub25jZT0iT0E2TUc5dEVRR20yaGgiLG5jPTAw + MDAwMDAxLGNub25jZT0iT0E2TUhYaDZWcVRyUmsiLGRpZ2VzdC11cmk9Im + ltYXAvZWx3b29kLmlubm9zb2Z0LmNvbSIscmVzcG9uc2U9ZDM4OGRhZDkw + ZDRiYmQ3NjBhMTUyMzIxZjIxNDNhZjcscW9wPWF1dGg=" + S: OK (SASL "cnNwYXV0aD1lYTQwZjYwMzM1YzQyN2I1NTI3Yjg0ZGJhYmNkZmZmZA==") + + A slightly different variant of the same authentication exchange: + + S: "IMPLEMENTATION" "CMU Cyrus Sieved v001" + S: "SASL" "DIGEST-MD5 GSSAPI" + S: "SIEVE" "FILEINTO VACATION" + S: "STARTTLS" + S: OK + C: Authenticate "DIGEST-MD5" + S: {128+} + S: cmVhbG09ImVsd29vZC5pbm5vc29mdC5jb20iLG5vbmNlPSJPQTZNRzl0 + RVFHbTJoaCIscW9wPSJhdXRoIixhbGdvcml0aG09bWQ1LXNlc3MsY2hh + cnNldD11dGYtOA== + C: {276+} + C: Y2hhcnNldD11dGYtOCx1c2VybmFtZT0iY2hyaXMiLHJlYWxtPSJlbHdvb2 + QuaW5ub3NvZnQuY29tIixub25jZT0iT0E2TUc5dEVRR20yaGgiLG5jPTAw + MDAwMDAxLGNub25jZT0iT0E2TUhYaDZWcVRyUmsiLGRpZ2VzdC11cmk9Im + ltYXAvZWx3b29kLmlubm9zb2Z0LmNvbSIscmVzcG9uc2U9ZDM4OGRhZDkw + ZDRiYmQ3NjBhMTUyMzIxZjIxNDNhZjcscW9wPWF1dGg=" + S: {56+} + S: cnNwYXV0aD1lYTQwZjYwMzM1YzQyN2I1NTI3Yjg0ZGJhYmNkZmZmZA== + C: "" + S: OK + + Another example demostrating use of SASL PLAIN mechanism under TLS: + + S: "IMPLEMENTATION" "CMU Cyrus Sieved v001" + S: "SASL" "" +<<Is this allowed?>> + S: "SIEVE" "FILEINTO VACATION" + S: "STARTTLS" + S: OK + C: STARTTLS + S: OK + <TLS negotiation, further commands are under TLS layer> + S: "IMPLEMENTATION" "CMU Cyrus Sieved v001" + S: "SASL" "PLAIN" + S: "SIEVE" "FILEINTO VACATION" + S: OK + C: Authenticate "PLAIN" "QJIrweAPyo6Q1T9xu" + S: NO + C: Authenticate "PLAIN" "QJIrweAPyo6Q1T9xz" + S: NO + C: Authenticate "PLAIN" "QJIrweAPyo6Q1T9xy" + S: BYE "Too many failed authentication attempts" + <Server closes connection> + + +2.2. STARTTLS Command + + Support for STARTTLS command in servers is optional. Its availability + is advertised with "STARTTLS" capability as described in section + 1.8. + + The STARTTLS command requests commencement of a TLS negotiation. + The negotiation begins immediately after the CRLF in the OK + response. After a client issues a STARTTLS command, it MUST NOT + issue further commands until a server response is seen and the TLS + negotiation is complete. + + The STARTTLS command is only valid in non-authenticated state. The + server remains in non-authenticated state, even if client + credentials are supplied during the TLS negotiation. The SASL [SASL] + EXTERNAL mechanism MAY be used to authenticate once TLS client + credentials are successfully exchanged, but servers supporting the + STARTTLS command are not required to support the EXTERNAL mechanism. + + After the TLS layer is established, the server MUST re-issue the + capability results, followed by an OK response. This is necessary to + protect against man-in-the-middle attacks which alter the capabilities + list prior to STARTTLS. + + The capability result MUST NOT include the STARTTLS capability. + + The client MUST discard cached capability information and replace it + with the new information. The server MAY advertise different + capabilities after STARTTLS. + + Example: + + C: STARTTLS + S: OK + <TLS negotiation, further commands are under TLS layer> + S: "IMPLEMENTATION" "CMU Cyrus Sieved v001" + S: "SASL" "PLAIN DIGEST-MD5 GSSAPI" + S: "SIEVE" "FILEINTO VACATION" + S: OK + + +2.3. LOGOUT Command + + The client sends the LOGOUT command when it is finished with a + connection and wishes to terminate it. The server MUST reply with an + OK response and terminate the connection. The server MUST ignore + commands issued by the client after the LOGOUT command. + + Example: + + C: Logout + S: OK + <connection terminated> + + +2.4. CAPABILITY Command + + The CAPABILITY command requests the server capabilities as described + earlier in this document. While the capabilities are sent upon + connection, they may change during authentication. The client SHOULD + issue a CAPABILITY command after successful authentication or after + negotiating a security layer using STARTTLS. + + + Example: + + C: CAPABILITY + S: "IMPLEMENTATION" "CMU Cyrus Sieved v001" + S: "SASL" "PLAIN KERBEROS_V4 GSSAPI" + S: "SIEVE" "FILEINTO VACATION" + S: "STARTTLS" + S: OK + + +2.5. HAVESPACE Command + + Arguments: + String - name + Number - size + + The HAVESPACE command is used to query the server for available + space. Clients specify the name they wish to save the script as and + it's size in octets. Servers respond with an NO if storing a script + with that name and size would fail or OK otherwise. Clients should + issue this command before attempting to place a script on the + server. + + Example: + + C: HAVESPACE "myscript" 999999 + S: NO (QUOTA) "Quota exceeded" + + C: HAVESPACE "foobar" 435 + S: OK + + +2.6. PUTSCRIPT Command + + Arguments: + String - Script name + String - Script content + + The PUTSCRIPT command is used by the client to submit a Sieve script + to the server. + + If the script already exists upon success the old script will be + overwritten. The old script MUST NOT be overwritten if PUTSCRIPT + fails in any way. A script of zero length SHOULD be disallowed. + + This command places the script on the server. It does not affect + whether the script is processed on incoming mail. The SETACTIVE + command is used to mark a script as active. + + When submitting large scripts clients SHOULD use the HAVESPACE + command beforehand to query if the server is willing to accept a + script of that size. + + The server MUST check the submitted script for syntactic validity. + If the script fails this test the server MUST reply with a NO + response. Any script that fails the validity test MUST NOT be stored + on the server. The message given with a NO response MUST be human + readable and SHOULD contain a specific error message giving line + number of the first error. Implementors should strive to produce + helpful error messages similar to those given by programming + language compilers. Client implementations should note that this may + be a multiline literal string with more than one error message + separated by newlines. + + Example: + + C: Putscript "foo" {31+} + C: #comment + C: InvalidSieveCommand + C: + S: NO "line 2: Syntax error" + + C: Putscript "mysievescript" {110+} + C: require ["fileinto"]; + C: + C: if envelope :contains "to" "tmartin+sent" { + C: fileinto "INBOX.sent"; + C: } + S: OK + + +2.7. LISTSCRIPTS Command + + This command lists the scripts the user has on the server. Upon + success a list of linebreak separated script names is returned + followed by an OK response. If there exists an active script the + atom ACTIVE is appended to the line of that script. The ACTIVE + string MUST NOT appear on more than one response line. + + Example: + + C: Listscripts + S: "summer_script" + S: "vacation_script" + S: "main_script" ACTIVE + S: OK + + +2.8. SETACTIVE Command + + Arguments: + String - script name + + This command sets a script active. If the script name is the empty + string (i.e. "") then any active script is disabled. Disabling an active script + when there is no script active is not an error and MUST result in OK reply. + + If the script does not exist on the server then the server MUST reply with a NO + response. + + Examples: + + C: Setactive "vacationscript" + S: Ok + + C: Setactive "" + S: Ok + + C: Setactive "baz" + S: No "There is no script by that name" + + +2.9. GETSCRIPT Command + + Arguments: + String - Script name + + This command gets the contents of the specified script. If the + script does not exist the server MUST reply with a NO response. Upon + success a string with the contents of the script is returned + followed by a OK response. + + Example: + + C: Getscript "myscript" + S: {48+} + S: #this is my wonderful script + S: reject "I reject all"; + S: + S: OK + + +2.10. DELETESCRIPT Command + + Parameters: + sieve-name - Script name + + This command is used to delete a user's Sieve script. Servers MUST + reply with a NO response if the script does not exist. The server + MUST NOT allow the client to delete an active script and reply with + a NO response if attempted. If a client wishes to delete an active + script it should use the SETACTIVE command to disable the script + first. + + Example: + + C: Deletescript "foo" + S: Ok + + C: Deletescript "baz" + S: No "You may not delete an active script" + + +3. Sieve URL Scheme + + URI scheme name: sieve + + Status: permanent + + URI scheme syntax: + + Described using ABNF [ABNF] and ABNF entities from [URI-GEN]. + + sieveurl = sieveurl-server / sieveurl-script + + sieveurl-server = "sieve://" authority + + sieveurl-script = "sieve://" [ authority ] "/" scriptname + + scriptname = *pchar + + URI scheme semantics: + + A Sieve URL identifies a Sieve server or a Sieve + script on a Sieve server. <<The latter form is associated with + application/sieve MIME type.>> + <<There is no MIME type associated with this URI.>> + + The server form is used in the REFERRAL response code in order + to designate another server where the client should perform + its operations. + + The script form allows to retrieve (GETSCRIPT), update (PUTSCRIPT), + delete (DELETESCRIPT) or activate (SETACTIVE) the named script, + however the most typical action would be to retrieve the script. + If the script name is empty, the URI requests that the client + lists available scripts using LISTSCRIPTS command. + + Encoding considerations: The script name, if present, + is in UTF-8. Non-US-ASCII UTF-8 octets MUST be percent-encoded as + described in [URI-GEN]. + + The user name (in the "authority" part), if present, + is in UTF-8. Non-US-ASCII UTF-8 octets MUST be percent-encoded as + described in [URI-GEN]. + + Applications/protocols that use this URI scheme name: + <<The protocol is described in this document.>> + + Interoperability considerations: None. + + Security considerations: <<None>>. + + Contact: Alexey Melnikov <alexey.melnikov@isode.com> + + Author/Change controller: IESG. + + References: This document and <<RFC 3028>>. + + +4. Formal Syntax + + The following syntax specification uses the augmented Backus-Naur + Form (BNF) notation as specified in [ABNF]. This uses the ABNF core + rules as specified in Appendix A of the ABNF specification [ABNF]. + + Except as noted otherwise, all alphabetic characters are case- + insensitive. The use of upper or lower case characters to define + token strings is for editorial clarity only. Implementations MUST + accept these strings in a case-insensitive fashion. + + + SAFE-CHAR = %x01-09 / %x0B-0C / %x0E-21 / %x23-5B / %x5D-7F + ;; any TEXT-CHAR except QUOTED-SPECIALS + + QUOTED-CHAR = SAFE-UTF8-CHAR / "\" QUOTED-SPECIALS + + QUOTED-SPECIALS = <"> / "\" + + SAFE-UTF8-CHAR = SAFE-CHAR / UTF8-2 / UTF8-3 / UTF8-4 / + UTF8-5 / UTF8-6 + + UTF8-1 = %x80-BF + + UTF8-2 = %xC0-DF UTF8-1 + + UTF8-3 = %xE0-EF 2UTF8-1 + + UTF8-4 = %xF0-F7 3UTF8-1 + + UTF8-5 = %xF8-FB 4UTF8-1 + + UTF8-6 = %xFC-FD 5UTF8-1 + + auth-type = <"> auth-type-name <"> + + auth-type-name = iana-token + ;; as defined in SASL [SASL] + + command = command-authenticate / command-logout / + command-getscript / command-setactive / + command-listscripts / command-deletescript / + command-putscript / command-capability / + command-havespace / command-starttls + + command-authenticate = "AUTHENTICATE" SP auth-type [SP string] + *(CRLF string) CRLF + + command-capability = "CAPABILITY" CRLF + + command-deletescript = "DELETESCRIPT" SP sieve-name CRLF + + command-getscript = "GETSCRIPT" SP sieve-name CRLF + + command-havespace = "HAVESPACE" SP sieve-name SP number CRLF + + command-listscripts = "LISTSCRIPTS" CRLF + + command-logout = "LOGOUT" CRLF + + command-putscript = "PUTSCRIPT" SP sieve-name SP string CRLF + + command-setactive = "SETACTIVE" SP sieve-name CRLF + + command-starttls = "STARTTLS" CRLF + + literal = "{" number "+}" CRLF *OCTET + ;; The number represents the number of octets. + ;; Sieve scripts MUST be sent as literal-utf8. + ;; <<literal-utf8>> is defined in ACAP. + + number = 1*DIGIT + ;; A 32-bit unsigned number. + ;; (0 <= n < 4,294,967,296) + + quoted = <"> *1024QUOTED-CHAR <"> + ;; limited to 1024 octets between the <">s + + resp-code = "AUTH-TOO-WEAK" / "ENCRYPT-NEEDED" / + "QUOTA" / resp-code-sasl / resp-code-referral + "TRANSITION-NEEDED" / "TRYLATER" / + resp-code-ext + + resp-code-referral = "REFERRAL" SP sieveurl + + resp-code-sasl = "SASL" SP string + + resp-code-ext = iana-token [SP extension-data] + ;; unknown codes MUST be tolerated by the client + + response = response-authenticate / response-logout / + response-getscript / response-setactive / + response-listscripts / response-deletescript / + response-putscript / response-capability / + response-havespace / response-starttls + + response-authenticate = *(string CRLF) (response-oknobye) + + response-capability = *(single-capability) response-oknobye + + single-capability = capability-name [SP string] CRLF + + capability-name = string + <<Note that literals are allowed!>> + + initial-capabilities = <"> "IMPLEMENTATION" <"> SP string / + <"> "SASL" <"> SP sasl-mechs / + <"> "SIEVE" <"> SP sieve-extensions / + <"> "STARTTLS" <"> + ;; Each capability conforms to + ;; the syntax for single-capability. + ;; Also note that the capability name + ;; can be returned as either literal + ;; or quoted, even though only "quoted" + ;; string is shown above. + + sasl-mechs = string + ; space separated list of SASL mechanisms, + ; can be empty + + sieve-extensions = string + ; space separated list of supported SIEVE extensions, + ; can be empty + + response-deletescript = response-oknobye + + response-getscript = [string CRLF] response-oknobye + + response-havespace = response-oknobye + + response-listscripts = *(sieve-name [SP "ACTIVE"] CRLF) response-oknobye + ;; ACTIVE may only occur with one sieve-name + + response-logout = response-oknobye + + response-oknobye = ("OK" / "NO" / "BYE") [SP "(" resp-code ")"] + [SP string] CRLF + + response-putscript = response-oknobye + + response-setactive = response-oknobye + + response-starttls = response-oknobye + + sieve-name = string + + string = quoted / literal + + +5. Security Considerations + + The AUTHENTICATE command uses SASL [SASL] and possibly TLS [TLS] to provide + basic authentication, authorization, integrity and privacy services. + When a SASL mechanism is used the security considerations for that + mechanism apply. + + This protocol transactions are susceptible to passive observers or + man in the middle attacks which alter the data, unless the optional + encryption and integrity services of the AUTHENTICATE command are + enabled, or an external security mechanism is used for protection. + It may be useful to allow configuration of both clients and servers + to refuse to transfer sensitive information in the absence of strong + encryption. + + +6. IANA Considerations + + IANA is requested to reserve TCP port number 2000 for use with + the Manage Sieve protocol described in this document. + + IANA is requested to create a new registry for Manage Sieve + capabilities. The registration template for Manage Sieve capabilities + is specified in the next section. + Manage Sieve protocol capabilities MUST be specified in a standards + track or IESG approved experimental RFC. + + <<Add a new registry for response codes, as per ABNF comments.>> + + <<Reference to SIEVE URL registration.>> + + +6.1. Manage Sieve Capability Registration Template + + To: iana@iana.org + Subject: Manage Sieve Capability Registration + + Please register the following Manage Sieve Capability: + + Capability name: + + Description: + + Relevant publications: + + Person & email address to contact for further information: + + Author/Change controller: + + +6.2. Registration of Initial Manage Sieve capabilities. + + To: iana@iana.org + Subject: Manage Sieve Capability Registration + + Please register the following Manage Sieve Capability: + + Capability name: IMPLEMENTATION + + Description: Its value contains name of server + implementation and its version. + + Relevant publications: this RFC, section 1.8. + + Person & email address to contact for further information: + Alexey Melnikov <alexey.melnikov@isode.com> + + Author/Change controller: IESG. + + + To: iana@iana.org + Subject: Manage Sieve Capability Registration + + Please register the following Manage Sieve Capability: + + Capability name: SASL + + Description: Its value contains a space separated + list of SASL mechanisms supported by server. + + Relevant publications: this RFC, sections 1.8 and 2.1. + + Person & email address to contact for further information: + Alexey Melnikov <alexey.melnikov@isode.com> + + Author/Change controller: IESG. + + + To: iana@iana.org + Subject: Manage Sieve Capability Registration + + Please register the following Manage Sieve Capability: + + Capability name: SIEVE + + Description: Its value contains a space separated + list of supported SIEVE extensions + + Relevant publications: this RFC, section 1.8. + <<Also [SIEVE]>> + + Person & email address to contact for further information: + Alexey Melnikov <alexey.melnikov@isode.com> + + Author/Change controller: IESG. + + + To: iana@iana.org + Subject: Manage Sieve Capability Registration + + Please register the following Manage Sieve Capability: + + Capability name: STARTTLS + + Description: This capability is returned if server + supports TLS (STARTTLS command). + + Relevant publications: this RFC, sections 1.8 and 2.2. + + Person & email address to contact for further information: + Alexey Melnikov <alexey.melnikov@isode.com> + + Author/Change controller: IESG. + + +7. References + +7.1. Normative References + + [KEYWORDS] S. Bradner, "Key words for use in RFCs to Indicate + Requirement Levels", RFC 2119, March 1997 + <ftp://ftp.isi.edu/in-notes/rfc2119.txt> + + [ABNF] Crocker, D. and P. Overell, "Augmented BNF for Syntax + Specifications: ABNF", RFC 4234, October 2005. + + [ACAP] Newman, Myers, "ACAP -- Application Configuration Access Proto- + col", RFC 2244, Innosoft, Netscape, November 1997. + + [SASL] Melnikov, A. and K. Zeilenga, "Simple Authentication and Security + Layer (SASL)", work in progress, draft-ietf-sasl-rfc2222bis-*.txt. + + [SASLprep] Zeilega, K., "SASLprep: Stringprep profile for user names + and passwords", work in progress, draft-ietf-sasl-saslprep-*.txt. + + [StringPrep] Hoffman, P. and Blanchet, M., "Preparation of + Internationalized Strings ("stringprep")", work in progress, + draft-hoffman-rfc3454bis-*.txt. + + [SASL-ANON] K. Zeilenga (Ed.), "The Anonymous SASL Mechanism", + work in progress, draft-ietf-sasl-anon-XX.txt (Obsoletes RFC 2245). + + [SIEVE] Guenther, P. and Showalter, T., "Sieve: An Email Filtering + Language", work in Progress, draft-ietf-sieve-3028bis-XX.txt + + [TLS] Dierks, T. and C. Allen, "The TLS Protocol Version 1.0", RFC 2246, + January 1999. <<Needs updating>> + + [URI-GEN] Berners-Lee, T., Fielding, R. and L. Masinter, + "Uniform Resource Identifier (URI): Generic Syntax", RFC 3986, + January 2005. + + +7.2. Informative References + + [IMAP4rev1] Crispin, M., "Internet Message Access Protocol - Version + 4rev1", RFC 3501, March 2003. + + [PLAIN] K. Zeilenga, "The Plain SASL Mechanism", + work in progress, draft-ietf-sasl-plain-XX.txt (Updates RFC 2595). + + +8. Author's Address + + Tim Martin + Mirapoint Inc. + 909 Hermosa Court + Sunnyvale, CA 94085 + Phone: (408) 720-3835 + EMail: timmartin@alumni.cmu.edu + + Alexey Melnikov + Isode Ltd. + 5 Castle Business Village + 36 Station Road + Hampton, Middlesex, TW12 2BX, GB + Email: alexey.melnikov@isode.com + + +Intellectual Property Statement + + The IETF takes no position regarding the validity or scope of any + Intellectual Property Rights or other rights that might be claimed to + pertain to the implementation or use of the technology described in + this document or the extent to which any license under such rights + might or might not be available; nor does it represent that it has + made any independent effort to identify any such rights. Information + on the procedures with respect to rights in RFC documents can be + found in BCP 78 and BCP 79. + + Copies of IPR disclosures made to the IETF Secretariat and any + assurances of licenses to be made available, or the result of an + attempt made to obtain a general license or permission for the use of + such proprietary rights by implementers or users of this + specification can be obtained from the IETF on-line IPR repository at + http://www.ietf.org/ipr. + + The IETF invites any interested party to bring to its attention any + copyrights, patents or patent applications, or other proprietary + rights that may cover technology that may be required to implement + this standard. Please address the information to the IETF at + ietf-ipr@ietf.org. + + The IETF has been notified of intellectual property rights claimed in + regard to some or all of the specification contained in this + document. For more information consult the online list of claimed + rights. + + +Disclaimer of Validity + + This document and the information contained herein are provided on an + "AS IS" basis and THE CONTRIBUTOR, THE ORGANIZATION HE/SHE REPRESENTS + OR IS SPONSORED BY (IF ANY), THE INTERNET SOCIETY AND THE INTERNET + ENGINEERING TASK FORCE DISCLAIM ALL WARRANTIES, EXPRESS OR IMPLIED, + INCLUDING BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE + INFORMATION HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED + WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + + +Copyright Statement + + Copyright (C) The Internet Society (2006). This document is subject + to the rights, licenses and restrictions contained in BCP 78, and + except as set forth therein, the authors retain all their rights. + +Acknowledgment + + Funding for the RFC Editor function is currently provided by the + Internet Society. + + +Appendix A. Acknowledgments + + Thanks to Simon Josefsson, Larry Greenfield, Allen Johnson, Chris + Newman, Lyndon Nerenberg, Tim Showalter, Sarah Robeson, and Walter + Wong for help with this document. diff --git a/src/common/libManageSieve/doc/Manage Sieve/draft-martin-managesieve-08.txt b/src/common/libManageSieve/doc/Manage Sieve/draft-martin-managesieve-08.txt new file mode 100644 index 00000000..59bb20f9 --- /dev/null +++ b/src/common/libManageSieve/doc/Manage Sieve/draft-martin-managesieve-08.txt @@ -0,0 +1,1359 @@ +Network Working Group Tim Martin +INTERNET-DRAFT BeThereBeSquare Inc. +Intended status: Standards Track Alexey Melnikov +Expires: February 2008 Isode Limited + August 14, 2007 + + + + A Protocol for Remotely Managing Sieve Scripts + <draft-martin-managesieve-08.txt> + +Status of this Memo + + By submitting this Internet-Draft, each author represents that any + applicable patent or other IPR claims of which he or she is aware + have been or will be disclosed, and any of which he or she becomes + aware will be disclosed, in accordance with Section 6 of BCP 79. + + Internet-Drafts are working documents of the Internet + Engineering Task Force (IETF), its areas, and its + working groups. Note that other groups may also distribute + working documents as Internet-Drafts. + + Internet-Drafts are draft documents valid for a maximum of + six months and may be updated, replaced, or obsoleted by + other documents at any time. It is inappropriate to use + Internet-Drafts as reference material or to cite them other + than as "work in progress." + + The list of current Internet-Drafts can be accessed at + http://www.ietf.org/1id-abstracts.html + + The list of Internet-Draft Shadow Directories can be accessed at + http://www.ietf.org/shadow.html + + Distribution of this memo is unlimited. + +Copyright Notice + + Copyright (C) The IETF Trust (2007). + + +Abstract + + Sieve scripts allow users to filter incoming email. Message stores + are commonly sealed servers so users cannot log into them, yet users + must be able to update their scripts on them. This document + describes a protocol "sieve" for securely managing Sieve scripts on + a remote server. This protocol allows a user to have multiple + scripts, and also alerts a user to syntactically flawed scripts. + + + Table of Contents + + + +Status of this Memo . . . . . . . . . . . . . . . . . . . . . . . . 1 +Abstract . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 +1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 3 +1.1. Changes . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 +1.2. Conventions Used in the Document . . . . . . . . . . . . . . 4 +1.3. Syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 +1.4. Response Codes . . . . . . . . . . . . . . . . . . . . . . . 5 +1.5. Active Script . . . . . . . . . . . . . . . . . . . . . . . . 6 +1.6. Quotas . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 +1.7. Script Names . . . . . . . . . . . . . . . . . . . . . . . . 7 +1.8. Capabilities . . . . . . . . . . . . . . . . . . . . . . . . 7 +2. Commands . . . . . . . . . . . . . . . . . . . . . . . . . . 8 +2.1. AUTHENTICATE Command . . . . . . . . . . . . . . . . . . . . 8 +2.2. STARTTLS Command . . . . . . . . . . . . . . . . . . . . . . 10 +2.3. LOGOUT Command . . . . . . . . . . . . . . . . . . . . . . . 10 +2.4. CAPABILITY Command . . . . . . . . . . . . . . . . . . . . . 11 +2.5. HAVESPACE Command . . . . . . . . . . . . . . . . . . . . . . 11 +2.6. PUTSCRIPT Command . . . . . . . . . . . . . . . . . . . . . . 11 +2.7. LISTSCRIPTS Command . . . . . . . . . . . . . . . . . . . . . 13 +2.8. SETACTIVE Command . . . . . . . . . . . . . . . . . . . . . . 13 +2.9. GETSCRIPT Command . . . . . . . . . . . . . . . . . . . . . . 13 +2.10. DELETESCRIPT Command . . . . . . . . . . . . . . . . . . . . 14 +3. Sieve URL Scheme . . . . . . . . . . . . . . . . . . . . . . 14 +4. Formal Syntax . . . . . . . . . . . . . . . . . . . . . . . . 15 +5. Security Considerations . . . . . . . . . . . . . . . . . . . 18 +7. References . . . . . . . . . . . . . . . . . . . . . . . . . +7.1. Normative References . . . . . . . . . . . . . . . . . . . . +7.2. Informative References . . . . . . . . . . . . . . . . . . . +8. Author's Address . . . . . . . . . . . . . . . . . . . . . . + + +1. Introduction + + + +1.1. Changes + + [[Note to RFC editor: please delete this section before + publication]] + + Changes since 07 + + -Fixed examples to match 3028bis - capability names are case + sensitive, so examples should show "fileinto" instead of + "FILEINTO", etc. + + -Minor editorial changes + + Changes since 06 + + -Clarified meaning of the QUOTA response code + + -Clarified which characters are not allowed in script names + and the maximum script name length + + -Clarified that the empty list of SASL mechanisms is allowed + + -Clarified that PUTSCRIPT must not store data after anonymous + authentication + + -Move text about NOTIFY capability into this document + + -Additional examples + + -Updated ABNF, References, Contact information + + Changes since 05 + + -More ABNF fixes + + -Added IANA considerations + + -Added/fixed text about AUTHENTICATE. + + -Updated the text om Sieve URLs. + + -Updated and added new examples. + + Changes since 04 + + -Updated boilerplate and some references. Added Alexey as co-editor. + + -Minor ABNF fixes + + -Cleaned up terminology (for example, made more consistent with + SASL) + + -Added more examples, fixed some existing examples + + -Clarified that STARTTLS command is optional + + -Clarified that disabling an active script when there is no script + active is not an error. + + Changes since 03 + + -Add referals and Sieve URLs + + -Lots of spelling/grammer fixes + + -Don't give capabilities after successful STARTTLS. This is because + it isn't consistant with AUTHENTICATE. There is language specifying + that a client should re-issue a CAPABILITY command after + AUTHENTICATE/STARTTLS. + + -Putting a script of length 0 doesn't remove the script. If this + functionality is desired, the DELETESCRIPT command should be used. + + Changes since 02 + + -add BYE response + + -typo on line 588 + + -allow ANONYMOUS access for sieve script verification + + -updated SIEVE spec reference + + Changes since 01 + + -changed contact info + + Changes since 00 + + -added response codes (from ACAP) + + -removed special-ok response from authenticate command (response + codes obsolete it) + + -changed service name to "sieve" + + -ABNF fixes + + -Alexey's wording changes + + -Eliminated lame PLAIN paragraph + + Changes since PRE + + -dropped synchronized literals. added HAVESPACE command + + -changed capability response syntax. added CAPABILITY command + + -allowed pipelining + + - "sieve" -> "Sieve". Other minor fixes + + -made script names more flexible + + -added starttls support + + +1.2. Conventions Used in the Document + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this + document are to be interpreted as described in [KEYWORDS]. + + In examples, "C:" and "S:" indicate lines sent by the client and + server respectively. Line breaks that do not start a new "C:" or + "S:" exist for editorial reasons. + + +1.3. Syntax + + This a line oriented protocol much like [IMAP4rev1] or [ACAP]. There + are three types: ATOMS, numbers and strings. Strings may be quoted + or literal. See [ACAP] for detailed descriptions of these types. + <<Clarify that the CRLF after the literal size is not "end of the + line">> + + Each command consists of an atom followed by zero or more strings + and numbers terminated by a newline. + + All client queries are replied to with either an OK, NO, or BYE + response. Each response may be followed by a response code (see + response codes section) and by a string consisting of human readable + text in the local language. The contents of the string SHOULD be + shown to the user and implementations MUST NOT attempt to parse the + message for meaning. + + The BYE response may be used if the server wishes to close the + connection. A server may wish to do this because the client was idle + for too long or there were too many failed authentication attempts. + This response can be issued at any time and should be immediately + followed by a server hang-up of the connection. If a server has a + inactivity timeout resulting in client autologout it MUST be no less + than 30 minutes. + + <<IANA registration is pending. Current implementations generally + use port number 2000.>> + + +1.4. Response Codes + + An OK, NO, or BYE response from the server MAY contain a response + code to describe the event in a more detailed machine parsable + fashion. A response code consists of data inside parentheses in the + form of an atom, possibly followed by a space and arguments. + Response codes are defined when there is a specific action that a + client can take based upon the additional information. In order to + support future extension, the response code is represented as a + slash-separated hierarchy with each level of hierarchy representing + increasing detail about the error. Clients MUST tolerate additional + hierarchical response code detail which they don't understand. + + The currently defined response codes are: + + AUTH-TOO-WEAK + + This response code is returned in the NO response from an + AUTHENTICATE command. It indicates that site security policy forbids + the use of the requested mechanism for the specified authentication + identity. + + ENCRYPT-NEEDED + + This response code is returned in the NO response from an + AUTHENTICATE command. It indicates that site security policy + requires the use of a strong encryption mechanism for the specified + authentication identity and mechanism. + + QUOTA + + If this response code is returned in the NO/BYE response, it means + that the command would have placed the user above the site-defined + quota constraints. If this response code is returned in the OK + response, it can mean that the user is near its quota or that the + user exceeded its quota, but the server supports soft quotas. + + REFERRAL + + This response code may be returned with a BYE result from any + command, and includes a mandatory parameter that indicates what + server to access to manage this user's sieve scripts. The server + will be specified by a Sieve URL (see "Sieve URL Scheme" section). + The scriptname portion of the URL MUST NOT be specified. The client + should authenticate to the specified server and use it for all + further commands in the current session. + + SASL + + This response code can occur in the OK response to a successful + AUTHENTICATE command and includes the optional final server response + data from the server as specified by [SASL]. + + TRANSITION-NEEDED + + This response code occurs in a NO response of an AUTHENTICATE + command. It indicates that the user name is valid, but the entry in + the authentication database needs to be updated in order to permit + authentication with the specified mechanism. This is typically done + by establishing a secure channel using TLS, followed by + authenticating once using the [PLAIN] authentication mechanism. + The selected mechanism SHOULD then work for authentications in + subsequent sessions. + + This condition can happen if a user has an entry in a system + authentication database such as Unix /etc/passwd, but does not have + credentials suitable for use by the specified mechanism. + + TRYLATER + + A command failed due to a temporary server failure. The client MAY + continue using local information and try the command later. This + response code only make sense when returned in a NO/BYE response. + + + Client implementations MUST tolerate response codes that they do not + recognize. + + +1.5. Active Script + + A user may have multiple Sieve scripts on the server, yet only one + script may be used for filtering of incoming messages. This is the + active script. Users may have zero or one active scripts and MUST + use the SETACTIVE command described below for changing the active + script or disabling Sieve processing. For example, a user may have + an everyday script they normally use and a special script they use + when they go on vacation. Users can change which script is being + used without having to download and upload a script stored somewhere + else. + + +1.6. Quotas + + Servers SHOULD impose quotas to prevent malicious users from + overflowing available storage. If a command would place a user over + a quota setting, servers that impose such quotas MUST reply with a + NO response. Client implementations MUST be able to handle commands + failing because of quota restrictions. + + +1.7. Script Names + + Sieve script names may contain any valid UTF-8 characters, except + for NUL, CR or LF. Names MUST be at least one octet long. Zero + octets script name has special meaning (see SETACTIVE command + section). Servers MUST allow names of up to 128 Unicode characters + in length, and MAY allow longer names. + + +1.8. Capabilities + + Server capabilities are sent by the server upon a client connection. + Clients may request the capabilities at a later time by issuing the + CAPABILITY command described later. The capabilities consist of a + series of lines each with one or two strings. The first string is + the name of the capability, which is case-insensitive. The second + optional string is the value associated with that capability. + Order of capabilities is arbitrary, but each capability name can + appear at most once. + + The following capabilities are defined in this document: + + IMPLEMENTATION - Name of implementation and version + + SASL - List of SASL mechanisms supported by the server, each + separated by a space. This list can be empty if and only if + STARTTLS is also advertised. This means that the client must + negotiate TLS encryption with STARTTLS first, at which point + the SASL capability will list a non empty list of SASL mechanisms. + <<But STARTTLS is optional!>> + + SIEVE - List of space separated Sieve extensions (as listed + in Sieve "require" action [SIEVE]) supported by the Sieve engine + + STARTTLS - If TLS [TLS] is supported by this implementation + + NOTIFY - A space separated list of URI schema parts for supported + notification methods. This capability MUST be specified if the + Sieve implementation supports the "enotify" extension + [NOTIFY]. + + A server implementation MUST return SIEVE and IMPLEMENTATION + capabilities. + + A client implementation MUST ignore any listed capabilities + that it does not understand. + + Example: + + S: "IMPlemENTATION" "Example1 ManageSieved v001" + S: "SASl" "DIGEST-MD5 GSSAPI" + S: "SIeVE" "fileinto vacation" + S: "StaRTTLS" + S: "NOTIFY" "xmpp mailto" + S: OK + + <<Add RENAMESCRIPT>> + + +2. Commands + + The following commands are valid. Prior to successful authentication + only the AUTHENTICATE, CAPABILITY, STARTTLS, and LOGOUT commands are + valid. Servers MUST reject all other commands with a NO response. + Clients may pipeline commands (send more than one command at a time + without waiting for completion of the first command ). However, a + group of commands sent together MUST NOT have an AUTHENTICATE, + a STARTTLS or a HAVESPACE command anywhere but the last command in + the list. + + +2.1. AUTHENTICATE Command + + Arguments: + String - mechanism + String - initial data (optional) + + The AUTHENTICATE command indicates a SASL [SASL] authentication + mechanism to the server. If the server supports the requested + authentication mechanism, it performs an authentication protocol + exchange to identify and authenticate the user. Optionally, it also + negotiates a security layer for subsequent protocol interactions. + If the requested authentication mechanism is not supported, the + server rejects the AUTHENTICATE command by sending the NO response. + + The authentication protocol exchange consists of a series of server + challenges and client responses that are specific to the selected + authentication mechanism. A server challenge consists of a string + (quoted or literal) followed by a CRLF. The contents of the string + is a base-64 encoding [BASE64] of the SASL data. A client response + consists of a string (quoted or literal) with the base-64 encoding + of the SASL data followed by a CRLF. If the client wishes to cancel + the authentication exchange, it issues a string containing a single + "*". If the server receives such a response, it MUST reject the + AUTHENTICATE command by sending an NO reply. + + Note that an empty challenge/response is sent as an empty string. + If the mechanism dictates that the final response is sent by the + server this data MAY be placed within the data portion of the SASL + response code to save a round trip. + + The optional initial-response argument to the AUTHENTICATE command + is used to save a round trip when using authentication mechanisms + that are defined to send no data in the initial challenge. When the + initial-response argument is used with such a mechanism, the initial + empty challenge is not sent to the client and the server uses the + data in the initial-response argument as if it were sent in response + to the empty challenge. If the initial-response argument to the + AUTHENTICATE command is used with a mechanism that sends data in the + initial challenge, the server rejects the AUTHENTICATE command by + sending the NO response. + + The service name specified by this protocol's profile of SASL is + "sieve". + + Reauthentication is not supported by ManageSieve protocol's profile + of SASL. I.e. after a successfully completed AUTHENTICATE command, + no more AUTHENTICATE commands may be issued in the same session. + After a successful AUTHENTICATE command completes, a server MUST + reject any further AUTHENTICATE commands with a NO reply. + + If a security layer is negotiated through the SASL authentication + exchange, it takes effect immediately following the CRLF that + concludes the authentication exchange for the client, and the CRLF + of the OK response for the server. + + When a security layer takes effect, the ManageSieve protocol is + reset to the initial state (the state in ManageSieve after a client + has connected to the server). The server MUST discard any + knowledge obtained from the client which was not obtained from + the SASL (or TLS) negotiation itself. + Likewise, the client MUST discard any knowledge obtained from + the server, such as the list of ManageSieve extensions, which + was not obtained from the SASL (or TLS) negotiation itself. + (Note that a client MAY compare the advertised SASL mechanisms + before and after authentication in order to detect an active + down-negotiation attack. See below.) + + Once a SASL security layer is established, the server MUST re-issue + the capability results, followed by an OK response. This is + necessary to protect against man-in-the-middle attacks which alter + the capabilities list prior to SASL negotiation. + The capability results MUST include all SASL mechanisms. This is + done in order to allow client to detect active down-negotiation + attack. + + When both [TLS] and SASL security layers are in effect, the + TLS encoding MUST be applied (when sending data) after the SASL + encoding, regardless of the order in which the layers were + negotiated. + + Server implementations SHOULD support SASL proxy authentication so + that an administrator can administer a user's scripts. Proxy + authentication is when a user authenticates as herself/himself but + requests the server to act (authorize) as another user. + + <<The authorization identity generated by this [SASL] exchange + is a simple username, and both client and server MUST use the + [SASLprep] profile of the [StringPrep] algorithm to prepare + these names for transmission or comparison. If preparation of + the authorization identity fails or results in an empty string + (unless it was transmitted as the empty string), the server + MUST fail the authentication.>> + + If an AUTHENTICATE command fails with a NO response, the client may + try another authentication mechanism by issuing another AUTHENTICATE + command. In other words, the client may request authentication + types in decreasing order of preference. + + Note that a failed NO response to the AUTHENTICATE command may + contain one of the following response codes: AUTH-TOO-WEAK, + ENCRYPT-NEEDED or TRANSITION-NEEDED. See section 1.4 for detailed + description of the relevant conditions. + + To ensure interoperability, client and server implementations + of this extension MUST implement the [DIGEST-MD5] SASL + mechanism. <<What is the IESG policy on this?>> + + + Implementations MAY advertise the ANONYMOUS SASL mechanism + [SASL-ANON]. This indicates that the server supports ANONYMOUS SIEVE + script syntax verification. Only the CAPABILITY, PUTSCRIPT and + LOGOUT commands are available to the anonymous user. All other + commands MUST give NO responses. Furthermore the PUTSCRIPT command + MUST NOT persistently store any data. In this mode a positive + response to the PUTSCRIPT command indicates that the given script + does not have any syntax errors. + + Examples (Note that long lines are folded for readability and are + not part of protocol exchange): + + S: "IMPLEMENTATION" "Example1 ManageSieved v001" + S: "SASL" "DIGEST-MD5 GSSAPI" + S: "SIEVE" "fileinto vacation" + S: "STARTTLS" + S: OK + C: Authenticate "DIGEST-MD5" + S: "cmVhbG09ImVsd29vZC5pbm5vc29mdC5jb20iLG5vbmNlPSJPQTZNRzl0 + RVFHbTJoaCIscW9wPSJhdXRoIixhbGdvcml0aG09bWQ1LXNlc3MsY2hh + cnNldD11dGYtOA==" + C: "Y2hhcnNldD11dGYtOCx1c2VybmFtZT0iY2hyaXMiLHJlYWxtPSJlbHdvb2 + QuaW5ub3NvZnQuY29tIixub25jZT0iT0E2TUc5dEVRR20yaGgiLG5jPTAw + MDAwMDAxLGNub25jZT0iT0E2TUhYaDZWcVRyUmsiLGRpZ2VzdC11cmk9Im + ltYXAvZWx3b29kLmlubm9zb2Z0LmNvbSIscmVzcG9uc2U9ZDM4OGRhZDkw + ZDRiYmQ3NjBhMTUyMzIxZjIxNDNhZjcscW9wPWF1dGg=" + S: OK (SASL "cnNwYXV0aD1lYTQwZjYwMzM1YzQyN2I1NTI3Yjg0ZGJhYmNkZ + mZmZA==") + + A slightly different variant of the same authentication exchange: + + S: "IMPLEMENTATION" "Example1 ManageSieved v001" + S: "SASL" "DIGEST-MD5 GSSAPI" + S: "SIEVE" "fileinto vacation" + S: "STARTTLS" + S: OK + C: Authenticate "DIGEST-MD5" + S: {128+} + S: cmVhbG09ImVsd29vZC5pbm5vc29mdC5jb20iLG5vbmNlPSJPQTZNRzl0 + RVFHbTJoaCIscW9wPSJhdXRoIixhbGdvcml0aG09bWQ1LXNlc3MsY2hh + cnNldD11dGYtOA== + C: {276+} + C: Y2hhcnNldD11dGYtOCx1c2VybmFtZT0iY2hyaXMiLHJlYWxtPSJlbHdvb2 + QuaW5ub3NvZnQuY29tIixub25jZT0iT0E2TUc5dEVRR20yaGgiLG5jPTAw + MDAwMDAxLGNub25jZT0iT0E2TUhYaDZWcVRyUmsiLGRpZ2VzdC11cmk9Im + ltYXAvZWx3b29kLmlubm9zb2Z0LmNvbSIscmVzcG9uc2U9ZDM4OGRhZDkw + ZDRiYmQ3NjBhMTUyMzIxZjIxNDNhZjcscW9wPWF1dGg=" + S: {56+} + S: cnNwYXV0aD1lYTQwZjYwMzM1YzQyN2I1NTI3Yjg0ZGJhYmNkZmZmZA== + C: "" + S: OK + + Another example demostrating use of SASL PLAIN mechanism under TLS. + This example also demonstrate use of SASL "initial response" + (the second parameter to the Authenticate command): + + S: "IMPLEMENTATION" "Example1 ManageSieved v001" + S: "SASL" "" + S: "SIEVE" "fileinto vacation" + S: "STARTTLS" + S: OK + C: STARTTLS + S: OK + <TLS negotiation, further commands are under TLS layer> + S: "IMPLEMENTATION" "Example1 ManageSieved v001" + S: "SASL" "PLAIN" + S: "SIEVE" "fileinto vacation" + S: OK + C: Authenticate "PLAIN" "QJIrweAPyo6Q1T9xu" + S: NO + C: Authenticate "PLAIN" "QJIrweAPyo6Q1T9xz" + S: NO + C: Authenticate "PLAIN" "QJIrweAPyo6Q1T9xy" + S: BYE "Too many failed authentication attempts" + <Server closes connection> + + The following example demonstrate use of SASL "initial response". + It also demonstrates that an empty response can be sent as a + literal: + + C: AUTHENTICATE "GSSAPI" {1488+} + C: YIIE[...1480 octets here ...]dA== + S: {208+} + S: YIGZBgkqhkiG9xIBAgICAG+BiTCBhqADAgEFoQMCAQ+iejB4oAMCARKic + [...114 octets here ...] + /yzpAy9p+Y0LanLskOTvMc0MnjgAa4YEr3eJ6 + C: {0+} + C: + S: {44+} + S: BQQF/wAMAAwAAAAAYRGFAo6W0vIHti8i1UXODgEAEAA= + C: {44+} + C: BQQE/wAMAAwAAAAAIsT1iv9UkZApw471iXt6cwEAAAE= + S: OK + + +2.2. STARTTLS Command + + Support for STARTTLS command in servers is optional. Its + availability is advertised with "STARTTLS" capability as described + in section 1.8. + + The STARTTLS command requests commencement of a TLS negotiation. + The negotiation begins immediately after the CRLF in the OK + response. After a client issues a STARTTLS command, it MUST NOT + issue further commands until a server response is seen and the TLS + negotiation is complete. + + The STARTTLS command is only valid in non-authenticated state. The + server remains in non-authenticated state, even if client + credentials are supplied during the TLS negotiation. The SASL [SASL] + EXTERNAL mechanism MAY be used to authenticate once TLS client + credentials are successfully exchanged, but servers supporting the + STARTTLS command are not required to support the EXTERNAL mechanism. + + After the TLS layer is established, the server MUST re-issue the + capability results, followed by an OK response. This is necessary to + protect against man-in-the-middle attacks which alter the + capabilities list prior to STARTTLS. This capability result MUST NOT + include the STARTTLS capability. + + The client MUST discard cached capability information and replace it + with the new information. The server MAY advertise different + capabilities after STARTTLS. + + Example: + + C: StartTls + S: oK + <TLS negotiation, further commands are under TLS layer> + S: "IMPLEMENTATION" "Example1 ManageSieved v001" + S: "SASL" "PLAIN DIGEST-MD5 GSSAPI" + S: "SIEVE" "fileinto vacation" + S: ok + + +2.3. LOGOUT Command + + The client sends the LOGOUT command when it is finished with a + connection and wishes to terminate it. The server MUST reply with an + OK response and terminate the connection. The server MUST ignore + commands issued by the client after the LOGOUT command. + + Example: + + C: Logout + S: Ok + <connection terminated> + + +2.4. CAPABILITY Command + + The CAPABILITY command requests the server capabilities as described + earlier in this document. While the capabilities are sent upon + connection, they may change during authentication. The client SHOULD + issue a CAPABILITY command after successful authentication or after + negotiating a security layer using STARTTLS. + + + Example: + + C: CAPABILITY + S: "IMPLEMENTATION" "Example1 ManageSieved v001" + S: "SASL" "PLAIN KERBEROS_V4 GSSAPI" + S: "SIEVE" "fileinto vacation" + S: "STARTTLS" + S: OK + + +2.5. HAVESPACE Command + + Arguments: + String - name + Number - size + + The HAVESPACE command is used to query the server for available + space. Clients specify the name they wish to save the script as and + its size in octets. Servers respond with an NO if storing a script + with that name and size would fail or OK otherwise. Clients SHOULD + issue this command before attempting to place a script on the + server. + + Example: + + C: HAVESPACE "myscript" 999999 + S: NO (QUOTA) "Quota exceeded" + + C: HAVESPACE "foobar" 435 + S: OK + + +2.6. PUTSCRIPT Command + + Arguments: + String - Script name + String - Script content + + The PUTSCRIPT command is used by the client to submit a Sieve script + to the server. + + If the script already exists, upon success the old script will be + overwritten. The old script MUST NOT be overwritten if PUTSCRIPT + fails in any way. A script of zero length SHOULD be disallowed. + + This command places the script on the server. It does not affect + whether the script is processed on incoming mail, unless it replaces + the script which is already active. The SETACTIVE + command is used to mark a script as active. + + When submitting large scripts clients SHOULD use the HAVESPACE + command beforehand to query if the server is willing to accept a + script of that size. + + The server MUST check the submitted script for syntactic validity. + If the script fails this test the server MUST reply with a NO + response. Any script that fails the validity test MUST NOT be stored + on the server. The message given with a NO response MUST be human + readable and SHOULD contain a specific error message giving the line + number of the first error. Implementors should strive to produce + helpful error messages similar to those given by programming + language compilers. Client implementations should note that this may + be a multiline literal string with more than one error message + separated by newlines. + + Example: + + C: Putscript "foo" {31+} + C: #comment + C: InvalidSieveCommand + C: + S: NO "line 2: Syntax error" + + C: Putscript "mysievescript" {110+} + C: require ["fileinto"]; + C: + C: if envelope :contains "to" "tmartin+sent" { + C: fileinto "INBOX.sent"; + C: } + S: OK + + +2.7. LISTSCRIPTS Command + + This command lists the scripts the user has on the server. Upon + success a list of CRLF separated script names (each represented + as a quoted or literal string) is returned followed by an OK + response. If there exists an active script the atom ACTIVE is + appended to the corresponding script name. The atom ACTIVE + MUST NOT appear on more than one response line. + + Example: + + C: Listscripts + S: "summer_script" + S: "vacation_script" + S: {13+} + S: clever"script + S: "main_script" ACTIVE + S: OK + + C: listscripts + S: "summer_script" + S: "main_script" active + S: OK + + +2.8. SETACTIVE Command + + Arguments: + String - script name + + This command sets a script active. If the script name is the empty + string (i.e. "") then any active script is disabled. Disabling an + active script when there is no script active is not an error and + MUST result in OK reply. + + If the script does not exist on the server then the server MUST + reply with a NO response. + + Examples: + + C: Setactive "vacationscript" + S: Ok + + C: Setactive "" + S: Ok + + C: Setactive "baz" + S: No "There is no script by that name" + + C: Setactive "baz" + S: No {31+} + S: There is no script by that name + + +2.9. GETSCRIPT Command + + Arguments: + String - Script name + + This command gets the contents of the specified script. If the + script does not exist the server MUST reply with a NO response. Upon + success a string with the contents of the script is returned + followed by a OK response. + + Example: + + C: Getscript "myscript" + S: {54+} + S: #this is my wonderful script + S: reject "I reject all"; + S: + S: OK + + +2.10. DELETESCRIPT Command + + Parameters: + sieve-name - Script name + + This command is used to delete a user's Sieve script. Servers MUST + reply with a NO response if the script does not exist. The server + MUST NOT allow the client to delete an active script, so the server + MUST reply with a NO response if attempted. If a client wishes to + delete an active script it should use the SETACTIVE command to + disable the script first. + + Example: + + C: Deletescript "foo" + S: Ok + + C: Deletescript "baz" + S: No "You may not delete an active script" + + +3. Sieve URL Scheme + + URI scheme name: sieve + + Status: permanent + + URI scheme syntax: + + Described using ABNF [ABNF] and ABNF entities from [URI-GEN]. + + sieveurl = sieveurl-server / sieveurl-script + + sieveurl-server = "sieve://" authority + + sieveurl-script = "sieve://" [ authority ] "/" scriptname + + scriptname = *pchar + + URI scheme semantics: + + A Sieve URL identifies a Sieve server or a Sieve + script on a Sieve server. <<The latter form is associated with + application/sieve MIME type.>> + <<There is no MIME type associated with this URI.>> + + The server form is used in the REFERRAL response code in order + to designate another server where the client should perform + its operations. + + The script form allows to retrieve (GETSCRIPT), update + (PUTSCRIPT), delete (DELETESCRIPT) or activate (SETACTIVE) + the named script, however the most typical action would be to + retrieve the script. If the script name is empty, the URI + requests that the client lists available scripts using + the LISTSCRIPTS command. + + Encoding considerations: The script name, if present, + is in UTF-8. Non-US-ASCII UTF-8 octets MUST be percent-encoded as + described in [URI-GEN]. + + The user name (in the "authority" part), if present, + is in UTF-8. Non-US-ASCII UTF-8 octets MUST be percent-encoded as + described in [URI-GEN]. + + Applications/protocols that use this URI scheme name: + ManageSieve [RFC XXXX] clients and servers. + Clients that can store user preferences in protocols such as + LDAP or ACAP. <<Add ref>> + + Interoperability considerations: None. + + Security considerations: <<None>>. + + Contact: Alexey Melnikov <alexey.melnikov@isode.com> + + Author/Change controller: IESG. + + References: This document and <<RFC 3028>> [SIEVE]. + + +4. Formal Syntax + + The following syntax specification uses the augmented Backus-Naur + Form (BNF) notation as specified in [ABNF]. This uses the ABNF core + rules as specified in Appendix A of the ABNF specification [ABNF]. + + Except as noted otherwise, all alphabetic characters are case- + insensitive. The use of upper or lower case characters to define + token strings is for editorial clarity only. Implementations MUST + accept these strings in a case-insensitive fashion. + + + SAFE-CHAR = %x01-09 / %x0B-0C / %x0E-21 / %x23-5B / + %x5D-7F + ;; any TEXT-CHAR except QUOTED-SPECIALS + + QUOTED-CHAR = SAFE-UTF8-CHAR / DQUOTE QUOTED-SPECIALS + + QUOTED-SPECIALS = DQUOTE / "\" + + SAFE-UTF8-CHAR = SAFE-CHAR / UTF8-2 / UTF8-3 / UTF8-4 / + UTF8-5 / UTF8-6 + + UTF8-1 = %x80-BF + + UTF8-2 = %xC0-DF UTF8-1 + + UTF8-3 = %xE0-EF 2UTF8-1 + + UTF8-4 = %xF0-F7 3UTF8-1 + + UTF8-5 = %xF8-FB 4UTF8-1 + + UTF8-6 = %xFC-FD 5UTF8-1 + + auth-type = DQUOTE auth-type-name DQUOTE + + auth-type-name = iana-token + ;; as defined in SASL [SASL] + + command = command-authenticate / command-logout / + command-getscript / command-setactive / + command-listscripts / command-deletescript / + command-putscript / command-capability / + command-havespace / command-starttls + + command-authenticate = "AUTHENTICATE" SP auth-type [SP string] + *(CRLF string) CRLF + + command-capability = "CAPABILITY" CRLF + + command-deletescript = "DELETESCRIPT" SP sieve-name CRLF + + command-getscript = "GETSCRIPT" SP sieve-name CRLF + + command-havespace = "HAVESPACE" SP sieve-name SP number CRLF + + command-listscripts = "LISTSCRIPTS" CRLF + + command-logout = "LOGOUT" CRLF + + command-putscript = "PUTSCRIPT" SP sieve-name SP string CRLF + + command-setactive = "SETACTIVE" SP sieve-name CRLF + + command-starttls = "STARTTLS" CRLF + + literal = "{" number "+}" CRLF *OCTET + ;; The number represents the number of + ;; octets. +<<Need to make "+" only allowed from clients to server, to match + IMAP LITERAL+). Also need to also update the examples to match.>> + + number = 1*DIGIT + ;; A 32-bit unsigned number. + ;; (0 <= n < 4,294,967,296) + + quoted = DQUOTE *1024QUOTED-CHAR DQUOTE + ;; limited to 1024 octets between the <">s + + resp-code = "AUTH-TOO-WEAK" / "ENCRYPT-NEEDED" / + "QUOTA" / resp-code-sasl / + resp-code-referral / + "TRANSITION-NEEDED" / "TRYLATER" / + resp-code-ext + + resp-code-referral = "REFERRAL" SP sieveurl + + resp-code-sasl = "SASL" SP string + + resp-code-ext = iana-token [SP extension-data] + ;; unknown response codes MUST be tolerated + ;; by the client. <<"iana-token" and + ;; "extension-data" are defined in ACAP>> + + response = response-authenticate / + response-logout / + response-getscript / + response-setactive / + response-listscripts / + response-deletescript / + response-putscript / + response-capability / + response-havespace / + response-starttls + + response-authenticate = *(string CRLF) (response-oknobye) + + response-capability = *(single-capability) response-oknobye + + single-capability = capability-name [SP string] CRLF + + capability-name = string + <<Note that literals are allowed!>> + + initial-capabilities = DQUOTE "IMPLEMENTATION" DQUOTE SP string / + DQUOTE "SASL" DQUOTE SP sasl-mechs / + DQUOTE "SIEVE" DQUOTE SP sieve-extensions / + DQUOTE "STARTTLS" DQUOTE + ;; Each capability conforms to + ;; the syntax for single-capability. + ;; Also note that the capability name + ;; can be returned as either literal + ;; or quoted, even though only "quoted" + ;; string is shown above. + + sasl-mechs = string + ; space separated list of SASL mechanisms, + ; can be empty + + sieve-extensions = string + ; space separated list of supported SIEVE extensions, + ; can be empty + + response-deletescript = response-oknobye + + response-getscript = [string CRLF] response-oknobye + <<Need to rewrite ABNF to make it clear that + script content is required with OK response + and must not be sent with the NO (and + possibly BYE) response.>> + + response-havespace = response-oknobye + + response-listscripts = *(sieve-name [SP "ACTIVE"] CRLF) + response-oknobye + ;; ACTIVE may only occur with one sieve-name + + response-logout = response-oknobye + + response-oknobye = ("OK" / "NO" / "BYE") [SP "(" resp-code ")"] + [SP string] CRLF + + response-putscript = response-oknobye + + response-setactive = response-oknobye + + response-starttls = response-oknobye + + sieve-name = string + ;; MUST NOT contain NUL, CR or LF + + string = quoted / literal + + +5. Security Considerations + + The AUTHENTICATE command uses SASL [SASL] to provide authentication + and authorization services. + Integrity and privacy services can be provided by [SASL] and/or + [TLS]. When a SASL mechanism is used the security considerations for + that mechanism apply. + + This protocol's transactions are susceptible to passive observers or + man in the middle attacks which alter the data, unless the optional + encryption and integrity services of the SASL (via the AUTHENTICATE + command) and/or [TLS] (via the STARTTLS command) are enabled, or an + external security mechanism is used for protection. It may be useful + to allow configuration of both clients and servers to refuse to + transfer sensitive information in the absence of strong encryption. + + +6. IANA Considerations + + IANA is requested to reserve TCP port number 2000 for use with + the Manage Sieve protocol described in this document. + + IANA is requested to create a new registry for Manage Sieve + capabilities. The registration template for Manage Sieve + capabilities is specified in the next section. + Manage Sieve protocol capabilities MUST be specified in a standards + track or IESG approved experimental RFC. + + <<Add a new registry for response codes, as per ABNF comments.>> + + <<Reference to SIEVE URL registration.>> + + +6.1. Manage Sieve Capability Registration Template + + To: iana@iana.org + Subject: Manage Sieve Capability Registration + + Please register the following Manage Sieve Capability: + + Capability name: + + Description: + + Relevant publications: + + Person & email address to contact for further information: + + Author/Change controller: + + +6.2. Registration of Initial Manage Sieve capabilities. + + To: iana@iana.org + Subject: Manage Sieve Capability Registration + + Please register the following Manage Sieve Capability: + + Capability name: IMPLEMENTATION + + Description: Its value contains name of server + implementation and its version. + + Relevant publications: this RFC, section 1.8. + + Person & email address to contact for further information: + Alexey Melnikov <alexey.melnikov@isode.com> + + Author/Change controller: IESG. + + + To: iana@iana.org + Subject: Manage Sieve Capability Registration + + Please register the following Manage Sieve Capability: + + Capability name: SASL + + Description: Its value contains a space separated + list of SASL mechanisms supported by server. + + Relevant publications: this RFC, sections 1.8 and 2.1. + + Person & email address to contact for further information: + Alexey Melnikov <alexey.melnikov@isode.com> + + Author/Change controller: IESG. + + + To: iana@iana.org + Subject: Manage Sieve Capability Registration + + Please register the following Manage Sieve Capability: + + Capability name: SIEVE + + Description: Its value contains a space separated + list of supported SIEVE extensions + + Relevant publications: this RFC, section 1.8. + <<Also [SIEVE]>> + + Person & email address to contact for further information: + Alexey Melnikov <alexey.melnikov@isode.com> + + Author/Change controller: IESG. + + + To: iana@iana.org + Subject: Manage Sieve Capability Registration + + Please register the following Manage Sieve Capability: + + Capability name: STARTTLS + + Description: This capability is returned if server + supports TLS (STARTTLS command). + + Relevant publications: this RFC, sections 1.8 and 2.2. + + Person & email address to contact for further information: + Alexey Melnikov <alexey.melnikov@isode.com> + + Author/Change controller: IESG. + + + To: iana@iana.org + Subject: Manage Sieve Capability Registration + + Please register the following Manage Sieve Capability: + + Capability name: NOTIFY + + Description: This capability is returned if server + supports 'enotify' Sieve extension. + + Relevant publications: this RFC, section 1.8. + + Person & email address to contact for further information: + Alexey Melnikov <alexey.melnikov@isode.com> + + Author/Change controller: IESG. + +7. References + +7.1. Normative References + + [KEYWORDS] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, March 1997. + + [ABNF] Crocker, D. and P. Overell, "Augmented BNF for Syntax + Specifications: ABNF", RFC 4234, October 2005. + + [ACAP] Newman, Myers, "ACAP -- Application Configuration Access + Protocol", RFC 2244, Innosoft, Netscape, November 1997. + + [SASL] Melnikov, A. and K. Zeilenga, "Simple Authentication and + Security Layer (SASL)", RFC 4422, June 2006. + + [SASLprep] Zeilega, K., "SASLprep: Stringprep profile for User Names + and Passwords", RFC 4013, February 2005. + + [StringPrep] Hoffman, P. and M. Blanchet, "Preparation of + Internationalized Strings ("stringprep")", RFC 3454, December 2002. + + [SASL-ANON] K. Zeilenga (Ed.), "Anonymous Simple Authentication and + Security Layer (SASL) Mechanism", RFC 4505, June 2006. + + [SIEVE] Guenther, P. and Showalter, T., "Sieve: An Email Filtering + Language", work in Progress, draft-ietf-sieve-3028bis-XX.txt + + [TLS] Dierks, T. and E. Rescorla, "The Transport Layer Security + (TLS) Protocol Version 1.1", RFC 4346, April 2006. + + [URI-GEN] Berners-Lee, T., Fielding, R. and L. Masinter, + "Uniform Resource Identifier (URI): Generic Syntax", RFC 3986, + January 2005. + + [BASE64] - Josefsson, S., "The Base16, Base32, and Base64 Data + Encodings", RFC 4648, October 2006. + + [NOTIFY] Melnikov, A. (Ed.), Leiba, B. (Ed.), Segmuller, W. and + T. Martin, "Sieve Extension: Notifications", work in progress, + draft-ietf-sieve-notify-XX.txt. + + +7.2. Informative References + + [IMAP4rev1] Crispin, M., "Internet Message Access Protocol - Version + 4rev1", RFC 3501, March 2003. + + [PLAIN] K. Zeilenga, "The PLAIN Simple Authentication and Security + Layer (SASL) Mechanism", RFC 4616, August 2006. + + [DIGEST-MD5] Melnikov, A. (Ed.), "Using Digest Authentication as + a SASL Mechanism", work in progress, + draft-ietf-sasl-rfc2831bis-XX.txt. + + +8. Author's Address + + Tim Martin + BeThereBeSquare Inc. + 672 Haight st. + San Francisco, CA 94117 + Phone: (510) 260-4175 + EMail: timmartin@alumni.cmu.edu + + Alexey Melnikov + Isode Ltd. + 5 Castle Business Village + 36 Station Road + Hampton, Middlesex, TW12 2BX, GB + Email: alexey.melnikov@isode.com + + +Intellectual Property + + The IETF takes no position regarding the validity or scope of any + Intellectual Property Rights or other rights that might be claimed + to pertain to the implementation or use of the technology described + in this document or the extent to which any license under such + rights might or might not be available; nor does it represent that + it has made any independent effort to identify any such rights. + Information on the procedures with respect to rights in RFC + documents can be found in BCP 78 and BCP 79. + + Copies of IPR disclosures made to the IETF Secretariat and any + assurances of licenses to be made available, or the result of an + attempt made to obtain a general license or permission for the use + of such proprietary rights by implementers or users of this + specification can be obtained from the IETF on-line IPR repository + at http://www.ietf.org/ipr. + + The IETF invites any interested party to bring to its attention any + copyrights, patents or patent applications, or other proprietary + rights that may cover technology that may be required to implement + this standard. Please address the information to the IETF at ietf- + ipr@ietf.org. + + +18. Full Copyright Statement + + Copyright (C) The IETF Trust (2007). + + This document is subject to the rights, licenses and restrictions + contained in BCP 78, and except as set forth therein, the authors + retain all their rights. + + This document and the information contained herein are provided on + an "AS IS" basis and THE CONTRIBUTOR, THE ORGANIZATION HE/SHE + REPRESENTS OR IS SPONSORED BY (IF ANY), THE INTERNET SOCIETY, THE + IETF TRUST AND THE INTERNET ENGINEERING TASK FORCE DISCLAIM ALL + WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY + WARRANTY THAT THE USE OF THE INFORMATION HEREIN WILL NOT INFRINGE + ANY RIGHTS OR ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS + FOR A PARTICULAR PURPOSE. + +Acknowledgement + + Funding for the RFC Editor function is currently provided by the + Internet Society. + + +Appendix A. Acknowledgments + + Thanks to Simon Josefsson, Larry Greenfield, Allen Johnson, Chris + Newman, Lyndon Nerenberg, Tim Showalter, Sarah Robeson, Walter + Wong, Barry Leiba, Arnt Gulbrandsen, Stephan Bosch for help with + this document. diff --git a/src/common/libManageSieve/doc/Manage Sieve/rfc5804.txt b/src/common/libManageSieve/doc/Manage Sieve/rfc5804.txt new file mode 100644 index 00000000..d6deaa89 --- /dev/null +++ b/src/common/libManageSieve/doc/Manage Sieve/rfc5804.txt @@ -0,0 +1,2747 @@ + + + + + + +Internet Engineering Task Force (IETF) A. Melnikov, Ed. +Request for Comments: 5804 Isode Limited +Category: Standards Track T. Martin +ISSN: 2070-1721 BeThereBeSquare, Inc. + July 2010 + + + A Protocol for Remotely Managing Sieve Scripts + +Abstract + + Sieve scripts allow users to filter incoming email. Message stores + are commonly sealed servers so users cannot log into them, yet users + must be able to update their scripts on them. This document + describes a protocol "ManageSieve" for securely managing Sieve + scripts on a remote server. This protocol allows a user to have + multiple scripts, and also alerts a user to syntactically flawed + scripts. + +Status of This Memo + + This is an Internet Standards Track document. + + This document is a product of the Internet Engineering Task Force + (IETF). It represents the consensus of the IETF community. It has + received public review and has been approved for publication by the + Internet Engineering Steering Group (IESG). Further information on + Internet Standards is available in Section 2 of RFC 5741. + + Information about the current status of this document, any errata, + and how to provide feedback on it may be obtained at + http://www.rfc-editor.org/info/rfc5804. + +Copyright Notice + + Copyright (c) 2010 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents + (http://trustee.ietf.org/license-info) in effect on the date of + publication of this document. Please review these documents + carefully, as they describe your rights and restrictions with respect + to this document. Code Components extracted from this document must + include Simplified BSD License text as described in Section 4.e of + the Trust Legal Provisions and are provided without warranty as + described in the Simplified BSD License. + + + + +Melnikov & Martin Standards Track [Page 1] + +RFC 5804 ManageSieve July 2010 + + +Table of Contents + + 1. Introduction ....................................................3 + 1.1. Commands and Responses .....................................3 + 1.2. Syntax .....................................................3 + 1.3. Response Codes .............................................3 + 1.4. Active Script ..............................................6 + 1.5. Quotas .....................................................6 + 1.6. Script Names ...............................................6 + 1.7. Capabilities ...............................................7 + 1.8. Transport ..................................................9 + 1.9. Conventions Used in This Document .........................10 + 2. Commands .......................................................10 + 2.1. AUTHENTICATE Command ......................................11 + 2.1.1. Use of SASL PLAIN Mechanism over TLS ...............16 + 2.2. STARTTLS Command ..........................................16 + 2.2.1. Server Identity Check ..............................17 + 2.3. LOGOUT Command ............................................20 + 2.4. CAPABILITY Command ........................................20 + 2.5. HAVESPACE Command .........................................20 + 2.6. PUTSCRIPT Command .........................................21 + 2.7. LISTSCRIPTS Command .......................................23 + 2.8. SETACTIVE Command .........................................24 + 2.9. GETSCRIPT Command .........................................25 + 2.10. DELETESCRIPT Command .....................................25 + 2.11. RENAMESCRIPT Command .....................................26 + 2.12. CHECKSCRIPT Command ......................................27 + 2.13. NOOP Command .............................................28 + 2.14. Recommended Extensions ...................................28 + 2.14.1. UNAUTHENTICATE Command ............................28 + 3. Sieve URL Scheme ...............................................29 + 4. Formal Syntax ..................................................31 + 5. Security Considerations ........................................37 + 6. IANA Considerations ............................................38 + 6.1. ManageSieve Capability Registration Template ..............39 + 6.2. Registration of Initial ManageSieve Capabilities ..........39 + 6.3. ManageSieve Response Code Registration Template ...........41 + 6.4. Registration of Initial ManageSieve Response Codes ........41 + 7. Internationalization Considerations ............................46 + 8. Acknowledgements ...............................................46 + 9. References .....................................................47 + 9.1. Normative References ......................................47 + 9.2. Informative References ....................................48 + + + + + + + + +Melnikov & Martin Standards Track [Page 2] + +RFC 5804 ManageSieve July 2010 + + +1. Introduction + +1.1. Commands and Responses + + A ManageSieve connection consists of the establishment of a client/ + server network connection, an initial greeting from the server, and + client/server interactions. These client/server interactions consist + of a client command, server data, and a server completion result + response. + + All interactions transmitted by client and server are in the form of + lines, that is, strings that end with a CRLF. The protocol receiver + of a ManageSieve client or server is either reading a line or reading + a sequence of octets with a known count followed by a line. + +1.2. Syntax + + ManageSieve is a line-oriented protocol much like [IMAP] or [ACAP], + which runs over TCP. There are three data types: atoms, numbers and + strings. Strings may be quoted or literal. See [ACAP] for detailed + descriptions of these types. + + Each command consists of an atom (the command name) followed by zero + or more strings and numbers terminated by CRLF. + + All client queries are replied to with either an OK, NO, or BYE + response. Each response may be followed by a response code (see + Section 1.3) and by a string consisting of human-readable text in the + local language (as returned by the LANGUAGE capability; see + Section 1.7), encoded in UTF-8 [UTF-8]. The contents of the string + SHOULD be shown to the user ,and implementations MUST NOT attempt to + parse the message for meaning. + + The BYE response SHOULD be used if the server wishes to close the + connection. A server may wish to do this because the client was idle + for too long or there were too many failed authentication attempts. + This response can be issued at any time and should be immediately + followed by a server hang-up of the connection. If a server has an + inactivity timeout resulting in client autologout, it MUST be no less + than 30 minutes after successful authentication. The inactivity + timeout MAY be less before authentication. + +1.3. Response Codes + + An OK, NO, or BYE response from the server MAY contain a response + code to describe the event in a more detailed machine-parsable + fashion. A response code consists of data inside parentheses in the + form of an atom, possibly followed by a space and arguments. + + + +Melnikov & Martin Standards Track [Page 3] + +RFC 5804 ManageSieve July 2010 + + + Response codes are defined when there is a specific action that a + client can take based upon the additional information. In order to + support future extension, the response code is represented as a + slash-separated (Solidus, %x2F) hierarchy with each level of + hierarchy representing increasing detail about the error. Response + codes MUST NOT start with the Solidus character. Clients MUST + tolerate additional hierarchical response code detail that they don't + understand. For example, if the client supports the "QUOTA" response + code, but doesn't understand the "QUOTA/MAXSCRIPTS" response code, it + should treat "QUOTA/MAXSCRIPTS" as "QUOTA". + + Client implementations MUST tolerate (ignore) response codes that + they do not recognize. + + The currently defined response codes are the following: + + AUTH-TOO-WEAK + + This response code is returned in the NO or BYE response from an + AUTHENTICATE command. It indicates that site security policy forbids + the use of the requested mechanism for the specified authentication + identity. + + ENCRYPT-NEEDED + + This response code is returned in the NO or BYE response from an + AUTHENTICATE command. It indicates that site security policy + requires the use of a strong encryption mechanism for the specified + authentication identity and mechanism. + + QUOTA + + If this response code is returned in the NO/BYE response, it means + that the command would have placed the user above the site-defined + quota constraints. If this response code is returned in the OK + response, it can mean that the user's storage is near its quota, or + it can mean that the account exceeded its quota but that the + condition is being allowed by the server (the server supports + so-called soft quotas). The QUOTA response code has two more + detailed variants: "QUOTA/MAXSCRIPTS" (the maximum number of per-user + scripts) and "QUOTA/MAXSIZE" (the maximum script size). + + REFERRAL + + This response code may be returned with a BYE result from any + command, and includes a mandatory parameter that indicates what + server to access to manage this user's Sieve scripts. The server + will be specified by a Sieve URL (see Section 3). The scriptname + + + +Melnikov & Martin Standards Track [Page 4] + +RFC 5804 ManageSieve July 2010 + + + portion of the URL MUST NOT be specified. The client should + authenticate to the specified server and use it for all further + commands in the current session. + + SASL + + This response code can occur in the OK response to a successful + AUTHENTICATE command and includes the optional final server response + data from the server as specified by [SASL]. + + TRANSITION-NEEDED + + This response code occurs in a NO response of an AUTHENTICATE + command. It indicates that the user name is valid, but the entry in + the authentication database needs to be updated in order to permit + authentication with the specified mechanism. This is typically done + by establishing a secure channel using TLS, verifying server identity + as specified in Section 2.2.1, and finally authenticating once using + the [PLAIN] authentication mechanism. The selected mechanism SHOULD + then work for authentications in subsequent sessions. + + This condition can happen if a user has an entry in a system + authentication database such as Unix /etc/passwd, but does not have + credentials suitable for use by the specified mechanism. + + TRYLATER + + A command failed due to a temporary server failure. The client MAY + continue using local information and try the command later. This + response code only makes sense when returned in a NO/BYE response. + + ACTIVE + + A command failed because it is not allowed on the active script, for + example, DELETESCRIPT on the active script. This response code only + makes sense when returned in a NO/BYE response. + + NONEXISTENT + + A command failed because the referenced script name doesn't exist. + This response code only makes sense when returned in a NO/BYE + response. + + ALREADYEXISTS + + A command failed because the referenced script name already exists. + This response code only makes sense when returned in a NO/BYE + response. + + + +Melnikov & Martin Standards Track [Page 5] + +RFC 5804 ManageSieve July 2010 + + + TAG + + This response code name is followed by a string specified in the + command. See Section 2.13 for a possible use case. + + WARNINGS + + This response code MAY be returned by the server in the OK response + (but it might be returned with the NO/BYE response as well) and + signals the client that even though the script is syntactically + valid, it might contain errors not intended by the script writer. + This response code is typically returned in response to PUTSCRIPT + and/or CHECKSCRIPT commands. A client seeing such response code + SHOULD present the returned warning text to the user. + +1.4. Active Script + + A user may have multiple Sieve scripts on the server, yet only one + script may be used for filtering of incoming messages. This is the + active script. Users may have zero or one active script and MUST use + the SETACTIVE command described below for changing the active script + or disabling Sieve processing. For example, users may have an + everyday script they normally use and a special script they use when + they go on vacation. Users can change which script is being used + without having to download and upload a script stored somewhere else. + +1.5. Quotas + + Servers SHOULD impose quotas to prevent malicious users from + overflowing available storage. If a command would place a user over + a quota setting, servers that impose such quotas MUST reply with a NO + response containing the QUOTA response code. Client implementations + MUST be able to handle commands failing because of quota + restrictions. + +1.6. Script Names + + A Sieve script name is a sequence of Unicode characters encoded in + UTF-8 [UTF-8]. A script name MUST comply with Net-Unicode Definition + (Section 2 of [NET-UNICODE]), with the additional restriction of + prohibiting the following Unicode characters: + + o 0000-001F; [CONTROL CHARACTERS] + + o 007F; DELETE + + o 0080-009F; [CONTROL CHARACTERS] + + + + +Melnikov & Martin Standards Track [Page 6] + +RFC 5804 ManageSieve July 2010 + + + o 2028; LINE SEPARATOR + + o 2029; PARAGRAPH SEPARATOR + + Sieve script names MUST be at least one octet (and hence Unicode + character) long. Zero octets script name has a special meaning (see + Section 2.8). Servers MUST allow names of up to 128 Unicode + characters in length (which can take up to 512 bytes when encoded in + UTF-8, not counting the terminating NUL), and MAY allow longer names. + A server that receives a script name longer than its internal limit + MUST reject the corresponding operation, in particular it MUST NOT + truncate the script name. + +1.7. Capabilities + + Server capabilities are sent automatically by the server upon a + client connection, or after successful STARTTLS and AUTHENTICATE + (which establishes a Simple Authentication and Security Layer (SASL)) + commands. Capabilities may change immediately after a successfully + completed STARTTLS command, and/or immediately after a successfully + completed AUTHENTICATE command, and/or after a successfully completed + UNAUTHENTICATE command (see Section 2.14.1). Capabilities MUST + remain static at all other times. + + Clients MAY request the capabilities at a later time by issuing the + CAPABILITY command described later. The capabilities consist of a + series of lines each with one or two strings. The first string is + the name of the capability, which is case-insensitive. The second + optional string is the value associated with that capability. Order + of capabilities is arbitrary, but each capability name can appear at + most once. + + The following capabilities are defined in this document: + + IMPLEMENTATION - Name of implementation and version. This capability + MUST always be returned by the server. + + SASL - List of SASL mechanisms supported by the server, each + separated by a space. This list can be empty if and only if STARTTLS + is also advertised. This means that the client must negotiate TLS + encryption with STARTTLS first, at which point the SASL capability + will list a non-empty list of SASL mechanisms. + + SIEVE - List of space-separated Sieve extensions (as listed in Sieve + "require" action [SIEVE]) supported by the Sieve engine. This + capability MUST always be returned by the server. + + + + + +Melnikov & Martin Standards Track [Page 7] + +RFC 5804 ManageSieve July 2010 + + + STARTTLS - If TLS [TLS] is supported by this implementation. Before + advertising this capability a server MUST verify to the best of its + ability that TLS can be successfully negotiated by a client with + common cipher suites. Specifically, a server should verify that a + server certificate has been installed and that the TLS subsystem has + successfully initialized. This capability SHOULD NOT be advertised + once STARTTLS or AUTHENTICATE command completes successfully. Client + and server implementations MUST implement the STARTTLS extension. + + MAXREDIRECTS - Specifies the limit on the number of Sieve "redirect" + actions a script can perform during a single evaluation. Note that + this is different from the total number of "redirect" actions a + script can contain. The value is a non-negative number represented + as a ManageSieve string. + + NOTIFY - A space-separated list of URI schema parts for supported + notification methods. This capability MUST be specified if the Sieve + implementation supports the "enotify" extension [NOTIFY]. + + LANGUAGE - The language (<Language-Tag> from [RFC5646]) currently + used for human-readable error messages. If this capability is not + returned, the "i-default" [RFC2277] language is assumed. Note that + the current language MAY be per-user configurable (i.e., it MAY + change after authentication). + + OWNER - The canonical name of the logged-in user (SASL "authorization + identity") encoded in UTF-8. This capability MUST NOT be returned in + unauthenticated state and SHOULD be returned once the AUTHENTICATE + command succeeds. + + VERSION - This capability MUST be returned by servers compliant with + this document or its successor. For servers compliant with this + document, the capability value is the string "1.0". Lack of this + capability means that the server predates this specification and thus + doesn't support the following commands: RENAMESCRIPT, CHECKSCRIPT, + and NOOP. + + Section 2.14 defines some additional ManageSieve extensions and their + respective capabilities. + + A server implementation MUST return SIEVE, IMPLEMENTATION, and + VERSION capabilities. + + A client implementation MUST ignore any listed capabilities that it + does not understand. + + + + + + +Melnikov & Martin Standards Track [Page 8] + +RFC 5804 ManageSieve July 2010 + + + Example: + + S: "IMPlemENTATION" "Example1 ManageSieved v001" + S: "SASl" "DIGEST-MD5 GSSAPI" + S: "SIeVE" "fileinto vacation" + S: "StaRTTLS" + S: "NOTIFY" "xmpp mailto" + S: "MAXREdIRECTS" "5" + S: "VERSION" "1.0" + S: OK + + After successful authentication, this might look like this: + + Example: + + S: "IMPlemENTATION" "Example1 ManageSieved v001" + S: "SASl" "DIGEST-MD5 GSSAPI" + S: "SIeVE" "fileinto vacation" + S: "NOTIFY" "xmpp mailto" + S: "OWNER" "alexey@example.com" + S: "MAXREdIRECTS" "5" + S: "VERSION" "1.0" + S: OK + +1.8. Transport + + The ManageSieve protocol assumes a reliable data stream such as that + provided by TCP. When TCP is used, a ManageSieve server typically + listens on port 4190. + + Before opening the TCP connection, the ManageSieve client first MUST + resolve the Domain Name System (DNS) hostname associated with the + receiving entity and determine the appropriate TCP port for + communication with the receiving entity. The process is as follows: + + 1. Attempt to resolve the hostname using a [DNS-SRV] Service of + "sieve" and a Proto of "tcp" for the target domain (e.g., + "example.net"), resulting in resource records such as + "_sieve._tcp.example.net.". The result of the SRV lookup, if + successful, will be one or more combinations of a port and + hostname; the ManageSieve client MUST resolve the returned + hostnames to IPv4/IPv6 addresses according to returned SRV record + weight. IP addresses from the first successfully resolved + hostname (with the corresponding port number returned by SRV + lookup) are used to connect to the server. If connection using + one of the IP addresses fails, the next resolved IP address is + + + + + +Melnikov & Martin Standards Track [Page 9] + +RFC 5804 ManageSieve July 2010 + + + used to connect. If connection to all resolved IP addresses + fails, then the resolution/connect is repeated for the next + hostname returned by SRV lookup. + + 2. If the SRV lookup fails, the fallback SHOULD be a normal IPv4 or + IPv6 address record resolution to determine the IP address, where + the port used is the default ManageSieve port of 4190. + +1.9. Conventions Used in This Document + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this + document are to be interpreted as described in [KEYWORDS]. + + In examples, "C:" and "S:" indicate lines sent by the client and + server respectively. Line breaks that do not start a new "C:" or + "S:" exist for editorial reasons. + + Examples of authentication in this document are using DIGEST-MD5 + [DIGEST-MD5] and GSSAPI [GSSAPI] SASL mechanisms. + +2. Commands + + This section and its subsections describe valid ManageSieve commands. + Upon initial connection to the server, the client's session is in + non-authenticated state. Prior to successful authentication, only + the AUTHENTICATE, CAPABILITY, STARTTLS, LOGOUT, and NOOP (see Section + 2.13) commands are valid. ManageSieve extensions MAY define other + commands that are valid in non-authenticated state. Servers MUST + reject all other commands with a NO response. Clients may pipeline + commands (send more than one command at a time without waiting for + completion of the first command). However, a group of commands sent + together MUST NOT have an AUTHENTICATE (*), a STARTTLS, or a + HAVESPACE command anywhere but the last command in the list. + + (*) - The only exception to this rule is when the AUTHENTICATE + command contains an initial response for a SASL mechanism that allows + clients to send data first, the mechanism is known to complete in one + round trip, and the mechanism doesn't negotiate a SASL security + layer. Two examples of such SASL mechanisms are PLAIN [PLAIN] and + EXTERNAL [SASL]. + + + + + + + + + + +Melnikov & Martin Standards Track [Page 10] + +RFC 5804 ManageSieve July 2010 + + +2.1. AUTHENTICATE Command + + Arguments: String - mechanism + String - initial data (optional) + + The AUTHENTICATE command indicates a SASL [SASL] authentication + mechanism to the server. If the server supports the requested + authentication mechanism, it performs an authentication protocol + exchange to identify and authenticate the user. Optionally, it also + negotiates a security layer for subsequent protocol interactions. If + the requested authentication mechanism is not supported, the server + rejects the AUTHENTICATE command by sending the NO response. + + The authentication protocol exchange consists of a series of server + challenges and client responses that are specific to the selected + authentication mechanism. A server challenge consists of a string + (quoted or literal) followed by a CRLF. The contents of the string + is a base-64 encoding [BASE64] of the SASL data. A client response + consists of a string (quoted or literal) with the base-64 encoding of + the SASL data followed by a CRLF. If the client wishes to cancel the + authentication exchange, it issues a string containing a single "*". + If the server receives such a response, it MUST reject the + AUTHENTICATE command by sending a NO reply. + + Note that an empty challenge/response is sent as an empty string. If + the mechanism dictates that the final response is sent by the server, + this data MAY be placed within the data portion of the SASL response + code to save a round trip. + + The optional initial-response argument to the AUTHENTICATE command is + used to save a round trip when using authentication mechanisms that + are defined to send no data in the initial challenge. When the + initial-response argument is used with such a mechanism, the initial + empty challenge is not sent to the client and the server uses the + data in the initial-response argument as if it were sent in response + to the empty challenge. If the initial-response argument to the + AUTHENTICATE command is used with a mechanism that sends data in the + initial challenge, the server MUST reject the AUTHENTICATE command by + sending the NO response. + + The service name specified by this protocol's profile of SASL is + "sieve". + + Reauthentication is not supported by ManageSieve protocol's profile + of SASL. That is, after a successfully completed AUTHENTICATE + command, no more AUTHENTICATE commands may be issued in the same + session. After a successful AUTHENTICATE command completes, a server + MUST reject any further AUTHENTICATE commands with a NO reply. + + + +Melnikov & Martin Standards Track [Page 11] + +RFC 5804 ManageSieve July 2010 + + + However, note that a server may implement the UNAUTHENTICATE + extension described in Section 2.14.1. + + If a security layer is negotiated through the SASL authentication + exchange, it takes effect immediately following the CRLF that + concludes the successful authentication exchange for the client, and + the CRLF of the OK response for the server. + + When a security layer takes effect, the ManageSieve protocol is reset + to the initial state (the state in ManageSieve after a client has + connected to the server). The server MUST discard any knowledge + obtained from the client that was not obtained from the SASL (or TLS) + negotiation itself. Likewise, the client MUST discard any knowledge + obtained from the server, such as the list of ManageSieve extensions, + that was not obtained from the SASL (and/or TLS) negotiation itself. + (Note that a client MAY compare the advertised SASL mechanisms before + and after authentication in order to detect an active down- + negotiation attack. See below.) + + Once a SASL security layer is established, the server MUST re-issue + the capability results, followed by an OK response. This is + necessary to protect against man-in-the-middle attacks that alter the + capabilities list prior to SASL negotiation. The capability results + MUST include all SASL mechanisms the server was capable of + negotiating with that client. This is done in order to allow the + client to detect an active down-negotiation attack. If a user- + oriented client detects such a down-negotiation attack, it SHOULD + either notify the user (it MAY give the user the opportunity to + continue with the ManageSieve session in this case) or close the + transport connection and indicate that a down-negotiation attack + might be in progress. If an automated client detects a down- + negotiation attack, it SHOULD return or log an error indicating that + a possible attack might be in progress and/or SHOULD close the + transport connection. + + When both [TLS] and SASL security layers are in effect, the TLS + encoding MUST be applied (when sending data) after the SASL encoding. + + Server implementations SHOULD support SASL proxy authentication so + that an administrator can administer a user's scripts. Proxy + authentication is when a user authenticates as herself/himself but + requests the server to act (authorize) as another user. + + The authorization identity generated by this [SASL] exchange is a + "simple username" (in the sense defined in [SASLprep]), and both + client and server MUST use the [SASLprep] profile of the [StringPrep] + algorithm to prepare these names for transmission or comparison. If + preparation of the authorization identity fails or results in an + + + +Melnikov & Martin Standards Track [Page 12] + +RFC 5804 ManageSieve July 2010 + + + empty string (unless it was transmitted as the empty string), the + server MUST fail the authentication. + + If an AUTHENTICATE command fails with a NO response, the client MAY + try another authentication mechanism by issuing another AUTHENTICATE + command. In other words, the client may request authentication types + in decreasing order of preference. + + Note that a failed (NO) response to the AUTHENTICATE command may + contain one of the following response codes: AUTH-TOO-WEAK, ENCRYPT- + NEEDED, or TRANSITION-NEEDED. See Section 1.3 for detailed + description of the relevant conditions. + + To ensure interoperability, both client and server implementations of + the ManageSieve protocol MUST implement the SCRAM-SHA-1 [SCRAM] SASL + mechanism, as well as [PLAIN] over [TLS]. + + Note: use of PLAIN over TLS reflects current use of PLAIN over TLS in + other email-related protocols; however, a longer-term goal is to + migrate email-related protocols from using PLAIN over TLS to SCRAM- + SHA-1 mechanism. + + Examples (Note that long lines are folded for readability and are not + part of protocol exchange): + + S: "IMPLEMENTATION" "Example1 ManageSieved v001" + S: "SASL" "DIGEST-MD5 GSSAPI" + S: "SIEVE" "fileinto vacation" + S: "STARTTLS" + S: "VERSION" "1.0" + S: OK + C: Authenticate "DIGEST-MD5" + S: "cmVhbG09ImVsd29vZC5pbm5vc29mdC5leGFtcGxlLmNvbSIsbm9uY2U9Ik + 9BNk1HOXRFUUdtMmhoIixxb3A9ImF1dGgiLGFsZ29yaXRobT1tZDUtc2Vz + cyxjaGFyc2V0PXV0Zi04" + C: "Y2hhcnNldD11dGYtOCx1c2VybmFtZT0iY2hyaXMiLHJlYWxtPSJlbHdvb2 + QuaW5ub3NvZnQuZXhhbXBsZS5jb20iLG5vbmNlPSJPQTZNRzl0RVFHbTJo + aCIsbmM9MDAwMDAwMDEsY25vbmNlPSJPQTZNSFhoNlZxVHJSayIsZGlnZX + N0LXVyaT0ic2lldmUvZWx3b29kLmlubm9zb2Z0LmV4YW1wbGUuY29tIixy + ZXNwb25zZT1kMzg4ZGFkOTBkNGJiZDc2MGExNTIzMjFmMjE0M2FmNyxxb3 + A9YXV0aA==" + S: OK (SASL "cnNwYXV0aD1lYTQwZjYwMzM1YzQyN2I1NTI3Yjg0ZGJhYmNkZ + mZmZA==") + + + + + + + + +Melnikov & Martin Standards Track [Page 13] + +RFC 5804 ManageSieve July 2010 + + + A slightly different variant of the same authentication exchange is: + + S: "IMPLEMENTATION" "Example1 ManageSieved v001" + S: "SASL" "DIGEST-MD5 GSSAPI" + S: "SIEVE" "fileinto vacation" + S: "VERSION" "1.0" + S: "STARTTLS" + S: OK + C: Authenticate "DIGEST-MD5" + S: {136} + S: cmVhbG09ImVsd29vZC5pbm5vc29mdC5leGFtcGxlLmNvbSIsbm9uY2U9Ik + 9BNk1HOXRFUUdtMmhoIixxb3A9ImF1dGgiLGFsZ29yaXRobT1tZDUtc2Vz + cyxjaGFyc2V0PXV0Zi04 + C: {300+} + C: Y2hhcnNldD11dGYtOCx1c2VybmFtZT0iY2hyaXMiLHJlYWxtPSJlbHdvb2 + QuaW5ub3NvZnQuZXhhbXBsZS5jb20iLG5vbmNlPSJPQTZNRzl0RVFHbTJo + aCIsbmM9MDAwMDAwMDEsY25vbmNlPSJPQTZNSFhoNlZxVHJSayIsZGlnZX + N0LXVyaT0ic2lldmUvZWx3b29kLmlubm9zb2Z0LmV4YW1wbGUuY29tIixy + ZXNwb25zZT1kMzg4ZGFkOTBkNGJiZDc2MGExNTIzMjFmMjE0M2FmNyxxb3 + A9YXV0aA== + S: {56} + S: cnNwYXV0aD1lYTQwZjYwMzM1YzQyN2I1NTI3Yjg0ZGJhYmNkZmZmZA== + C: "" + S: OK + + + + + + + + + + + + + + + + + + + + + + + + + + + +Melnikov & Martin Standards Track [Page 14] + +RFC 5804 ManageSieve July 2010 + + + Another example demonstrating use of SASL PLAIN mechanism under TLS + follows. This example also demonstrate use of SASL "initial + response" (the second parameter to the Authenticate command): + + S: "IMPLEMENTATION" "Example1 ManageSieved v001" + S: "VERSION" "1.0" + S: "SASL" "" + S: "SIEVE" "fileinto vacation" + S: "STARTTLS" + S: OK + C: STARTTLS + S: OK + <TLS negotiation, further commands are under TLS layer> + S: "IMPLEMENTATION" "Example1 ManageSieved v001" + S: "VERSION" "1.0" + S: "SASL" "PLAIN" + S: "SIEVE" "fileinto vacation" + S: OK + C: Authenticate "PLAIN" "QJIrweAPyo6Q1T9xu" + S: NO + C: Authenticate "PLAIN" "QJIrweAPyo6Q1T9xz" + S: NO + C: Authenticate "PLAIN" "QJIrweAPyo6Q1T9xy" + S: BYE "Too many failed authentication attempts" + <Server closes connection> + + + + + + + + + + + + + + + + + + + + + + + + + + +Melnikov & Martin Standards Track [Page 15] + +RFC 5804 ManageSieve July 2010 + + + The following example demonstrates use of SASL "initial response". + It also demonstrates that an empty response can be sent as a literal + and that negotiating a SASL security layer results in the server + re-issuing server capabilities: + + C: AUTHENTICATE "GSSAPI" {1488+} + C: YIIE[...1480 octets here ...]dA== + S: {208} + S: YIGZBgkqhkiG9xIBAgICAG+BiTCBhqADAgEFoQMCAQ+iejB4oAMCARKic + [...114 octets here ...] + /yzpAy9p+Y0LanLskOTvMc0MnjgAa4YEr3eJ6 + C: {0+} + C: + S: {44} + S: BQQF/wAMAAwAAAAAYRGFAo6W0vIHti8i1UXODgEAEAA= + C: {44+} + C: BQQE/wAMAAwAAAAAIsT1iv9UkZApw471iXt6cwEAAAE= + S: OK + <Further commands/responses are under SASL security layer> + S: "IMPLEMENTATION" "Example1 ManageSieved v001" + S: "VERSION" "1.0" + S: "SASL" "PLAIN DIGEST-MD5 GSSAPI" + S: "SIEVE" "fileinto vacation" + S: "LANGUAGE" "ru" + S: "MAXREDIRECTS" "3" + S: ok + +2.1.1. Use of SASL PLAIN Mechanism over TLS + + This section is normative for ManageSieve client implementations that + support SASL [PLAIN] over [TLS]. + + If a ManageSieve client is willing to use SASL PLAIN over TLS to + authenticate to the ManageSieve server, the client MUST verify the + server identity (see Section 2.2.1). If the server identity can't be + verified (e.g., the server has not provided any certificate, or if + the certificate verification fails), the client MUST NOT attempt to + authenticate using the SASL PLAIN mechanism. + +2.2. STARTTLS Command + + Support for STARTTLS command in servers is optional. Its + availability is advertised with "STARTTLS" capability as described in + Section 1.7. + + The STARTTLS command requests commencement of a TLS [TLS] + negotiation. The negotiation begins immediately after the CRLF in + the OK response. After a client issues a STARTTLS command, it MUST + + + +Melnikov & Martin Standards Track [Page 16] + +RFC 5804 ManageSieve July 2010 + + + NOT issue further commands until a server response is seen and the + TLS negotiation is complete. + + The STARTTLS command is only valid in non-authenticated state. The + server remains in non-authenticated state, even if client credentials + are supplied during the TLS negotiation. The SASL [SASL] EXTERNAL + mechanism MAY be used to authenticate once TLS client credentials are + successfully exchanged, but servers supporting the STARTTLS command + are not required to support the EXTERNAL mechanism. + + After the TLS layer is established, the server MUST re-issue the + capability results, followed by an OK response. This is necessary to + protect against man-in-the-middle attacks that alter the capabilities + list prior to STARTTLS. This capability result MUST NOT include the + STARTTLS capability. + + The client MUST discard cached capability information and replace it + with the new information. The server MAY advertise different + capabilities after STARTTLS. + + Example: + + C: StartTls + S: oK + <TLS negotiation, further commands are under TLS layer> + S: "IMPLEMENTATION" "Example1 ManageSieved v001" + S: "SASL" "PLAIN DIGEST-MD5 GSSAPI" + S: "SIEVE" "fileinto vacation" + S: "VERSION" "1.0" + S: "LANGUAGE" "fr" + S: ok + +2.2.1. Server Identity Check + + During the TLS negotiation, the ManageSieve client MUST check its + understanding of the server hostname/IP address against the server's + identity as presented in the server Certificate message, in order to + prevent man-in-the-middle attacks. In this section, the client's + understanding of the server's identity is called the "reference + identity". + + Checking is performed according to the following rules: + + o If the reference identity is a hostname: + + 1. If a subjectAltName extension of the SRVName [X509-SRV], + dNSName [X509] (in that order of preference) type is present + in the server's certificate, then it SHOULD be used as the + + + +Melnikov & Martin Standards Track [Page 17] + +RFC 5804 ManageSieve July 2010 + + + source of the server's identity. Matching is performed as + described in Section 2.2.1.1, with the exception that no + wildcard matching is allowed for SRVName type. If the + certificate contains multiple names (e.g., more than one + dNSName field), then a match with any one of the fields is + considered acceptable. + + 2. The client MAY use other types of subjectAltName for + performing comparison. + + 3. The server's identity MAY also be verified by comparing the + reference identity to the Common Name (CN) [RFC4519] value in + the leaf Relative Distinguished Name (RDN) of the subjectName + field of the server's certificate. This comparison is + performed using the rules for comparison of DNS names in + Section 2.2.1.1, below. Although the use of the Common Name + value is existing practice, it is deprecated, and + Certification Authorities are encouraged to provide + subjectAltName values instead. Note that the TLS + implementation may represent DNs in certificates according to + X.500 or other conventions. For example, some X.500 + implementations order the RDNs in a DN using a left-to-right + (most significant to least significant) convention instead of + LDAP's right-to-left convention. + + o When the reference identity is an IP address, the iPAddress + subjectAltName SHOULD be used by the client for comparison. The + comparison is performed as described in Section 2.2.1.2. + + If the server identity check fails, user-oriented clients SHOULD + either notify the user (clients MAY give the user the opportunity to + continue with the ManageSieve session in this case) or close the + transport connection and indicate that the server's identity is + suspect. Automated clients SHOULD return or log an error indicating + that the server's identity is suspect and/or SHOULD close the + transport connection. Automated clients MAY provide a configuration + setting that disables this check, but MUST provide a setting that + enables it. + + Beyond the server identity check described in this section, clients + should be prepared to do further checking to ensure that the server + is authorized to provide the service it is requested to provide. The + client may need to make use of local policy information in making + this determination. + + + + + + + +Melnikov & Martin Standards Track [Page 18] + +RFC 5804 ManageSieve July 2010 + + +2.2.1.1. Comparison of DNS Names + + If the reference identity is an internationalized domain name, + conforming implementations MUST convert it to the ASCII Compatible + Encoding (ACE) format as specified in Section 4 of RFC 3490 [RFC3490] + before comparison with subjectAltName values of type dNSName. + Specifically, conforming implementations MUST perform the conversion + operation specified in Section 4 of [RFC3490] as follows: + + o in step 1, the domain name SHALL be considered a "stored string"; + + o in step 3, set the flag called "UseSTD3ASCIIRules"; + + o in step 4, process each label with the "ToASCII" operation; and + + o in step 5, change all label separators to U+002E (full stop). + + After performing the "to-ASCII" conversion, the DNS labels and names + MUST be compared for equality according to the rules specified in + Section 3 of [RFC3490]; i.e., once all label separators are replaced + with U+002E (dot) they are compared in the case-insensitive manner. + + The '*' (ASCII 42) wildcard character is allowed in subjectAltName + values of type dNSName, and then only as the left-most (least + significant) DNS label in that value. This wildcard matches any + left-most DNS label in the server name. That is, the subject + *.example.com matches the server names a.example.com and + b.example.com, but does not match example.com or a.b.example.com. + +2.2.1.2. Comparison of IP Addresses + + When the reference identity is an IP address, the identity MUST be + converted to the "network byte order" octet string representation + [RFC791][RFC2460]. For IP Version 4, as specified in RFC 791, the + octet string will contain exactly four octets. For IP Version 6, as + specified in RFC 2460, the octet string will contain exactly sixteen + octets. This octet string is then compared against subjectAltName + values of type iPAddress. A match occurs if the reference identity + octet string and value octet strings are identical. + +2.2.1.3. Comparison of Other subjectName Types + + Client implementations MAY support matching against subjectAltName + values of other types as described in other documents. + + + + + + + +Melnikov & Martin Standards Track [Page 19] + +RFC 5804 ManageSieve July 2010 + + +2.3. LOGOUT Command + + The client sends the LOGOUT command when it is finished with a + connection and wishes to terminate it. The server MUST reply with an + OK response. The server MUST ignore commands issued by the client + after the LOGOUT command. + + The client SHOULD wait for the OK response before closing the + connection. This avoids the TCP connection going into the TIME_WAIT + state on the server. In order to avoid going into the TIME_WAIT TCP + state, the server MAY wait for a short while for the client to close + the TCP connection first. Whether or not the server waits for the + client to close the connection, it MUST then close the connection + itself. + + Example: + + C: Logout + S: Ok + <connection is terminated> + +2.4. CAPABILITY Command + + The CAPABILITY command requests the server capabilities as described + earlier in this document. It has no parameters. + + Example: + + C: CAPABILITY + S: "IMPLEMENTATION" "Example1 ManageSieved v001" + S: "VERSION" "1.0" + S: "SASL" "PLAIN SCRAM-SHA-1 GSSAPI" + S: "SIEVE" "fileinto vacation" + S: "STARTTLS" + S: OK + +2.5. HAVESPACE Command + + Arguments: String - name + Number - script size + + The HAVESPACE command is used to query the server for available + space. Clients specify the name they wish to save the script as and + its size in octets. Both parameters can be used by the server to see + if the script with the specified name and size is within a user's + quota(s). For example, the server MAY use the script name to check + if a script would be replaced or a new one would be created. Servers + respond with a NO if storing a script with that name and size would + + + +Melnikov & Martin Standards Track [Page 20] + +RFC 5804 ManageSieve July 2010 + + + fail or OK otherwise. Clients SHOULD issue this command before + attempting to place a script on the server. + + Note that the OK response from the HAVESPACE command does not + constitute a guarantee of success as server disk space conditions + could change between the client issuing the HAVESPACE and the client + issuing the PUTSCRIPT commands. A QUOTA response code (see + Section 1.3) remains a possible (albeit unlikely) response to a + subsequent PUTSCRIPT with the same name and size. + + Example: + + C: HAVESPACE "myscript" 999999 + S: NO (QUOTA/MAXSIZE) "Quota exceeded" + + C: HAVESPACE "foobar" 435 + S: OK + +2.6. PUTSCRIPT Command + + Arguments: String - Script name + String - Script content + + The PUTSCRIPT command is used by the client to submit a Sieve script + to the server. + + If the script already exists, upon success the old script will be + overwritten. The old script MUST NOT be overwritten if PUTSCRIPT + fails in any way. A script of zero length SHOULD be disallowed. + + This command places the script on the server. It does not affect + whether the script is processed on incoming mail, unless it replaces + the script that is already active. The SETACTIVE command is used to + mark a script as active. + + When submitting large scripts, clients SHOULD use the HAVESPACE + command beforehand to query if the server is willing to accept a + script of that size. + + The server MUST check the submitted script for validity, which + includes checking that the script complies with the Sieve grammar + [SIEVE] and that all Sieve extensions mentioned in the script's + "require" statement(s) are supported by the Sieve interpreter. (Note + that if the Sieve interpreter supports the Sieve "ihave" extension + [I-HAVE], any unrecognized/unsupported extension mentioned in the + "ihave" test MUST NOT cause the validation failure.) Other checks + such as validating the supplied command arguments for each command + MAY be performed. Essentially, the performed validation SHOULD be + + + +Melnikov & Martin Standards Track [Page 21] + +RFC 5804 ManageSieve July 2010 + + + the same as performed when compiling the script for execution. + Implementations that use a binary representation to store compiled + scripts can extend the validation to a full compilation, in order to + avoid validating uploaded scripts multiple times. + + If the script fails the validation, the server MUST reply with a NO + response. Any script that fails the validity test MUST NOT be stored + on the server. The message given with a NO response MUST be human + readable and SHOULD contain a specific error message giving the line + number of the first error. Implementors should strive to produce + helpful error messages similar to those given by programming language + compilers. Client implementations should note that this may be a + multiline literal string with more than one error message separated + by CRLFs. The human-readable message is in the language returned in + the latest LANGUAGE capability (or in "i-default"; see Section 1.7), + encoded in UTF-8 [UTF-8]. + + An OK response MAY contain the WARNINGS response code. In such a + case the human-readable message that follows the OK response SHOULD + contain a specific warning message (or messages) giving the line + number(s) in the script that might contain errors not intended by the + script writer. The human-readable message is in the language + returned in the latest LANGUAGE capability (or in "i-default"; see + Section 1.7), encoded in UTF-8 [UTF-8]. A client seeing such a + response code SHOULD present the message to the user. + + + + + + + + + + + + + + + + + + + + + + + + + + +Melnikov & Martin Standards Track [Page 22] + +RFC 5804 ManageSieve July 2010 + + + Examples: + + C: Putscript "foo" {31+} + C: #comment + C: InvalidSieveCommand + C: + S: NO "line 2: Syntax error" + + C: Putscript "mysievescript" {110+} + C: require ["fileinto"]; + C: + C: if envelope :contains "to" "tmartin+sent" { + C: fileinto "INBOX.sent"; + C: } + S: OK + + C: Putscript "myforwards" {190+} + C: redirect "111@example.net"; + C: + C: if size :under 10k { + C: redirect "mobile@cell.example.com"; + C: } + C: + C: if envelope :contains "to" "tmartin+lists" { + C: redirect "lists@groups.example.com"; + C: } + S: OK (WARNINGS) "line 8: server redirect action + limit is 2, this redirect might be ignored" + +2.7. LISTSCRIPTS Command + + This command lists the scripts the user has on the server. Upon + success, a list of CRLF-separated script names (each represented as a + quoted or literal string) is returned followed by an OK response. If + there exists an active script, the atom ACTIVE is appended to the + corresponding script name. The atom ACTIVE MUST NOT appear on more + than one response line. + + + + + + + + + + + + + + +Melnikov & Martin Standards Track [Page 23] + +RFC 5804 ManageSieve July 2010 + + + Example: + + C: Listscripts + S: "summer_script" + S: "vacation_script" + S: {13} + S: clever"script + S: "main_script" ACTIVE + S: OK + + C: listscripts + S: "summer_script" + S: "main_script" active + S: OK + +2.8. SETACTIVE Command + + Arguments: String - script name + + This command sets a script active. If the script name is the empty + string (i.e., ""), then any active script is disabled. Disabling an + active script when there is no script active is not an error and MUST + result in an OK reply. + + If the script does not exist on the server, then the server MUST + reply with a NO response. Such a reply SHOULD contain the + NONEXISTENT response code. + + Examples: + + C: Setactive "vacationscript" + S: Ok + + C: Setactive "" + S: Ok + + C: Setactive "baz" + S: No (NONEXISTENT) "There is no script by that name" + + C: Setactive "baz" + S: No (NONEXISTENT) {31} + S: There is no script by that name + + + + + + + + + +Melnikov & Martin Standards Track [Page 24] + +RFC 5804 ManageSieve July 2010 + + +2.9. GETSCRIPT Command + + Arguments: String - script name + + This command gets the contents of the specified script. If the + script does not exist, the server MUST reply with a NO response. + Such a reply SHOULD contain the NONEXISTENT response code. + + Upon success, a string with the contents of the script is returned + followed by an OK response. + + Example: + + C: Getscript "myscript" + S: {54} + S: #this is my wonderful script + S: reject "I reject all"; + S: + S: OK + +2.10. DELETESCRIPT Command + + Arguments: String - script name + + This command is used to delete a user's Sieve script. Servers MUST + reply with a NO response if the script does not exist. Such + responses SHOULD include the NONEXISTENT response code. + + The server MUST NOT allow the client to delete an active script, so + the server MUST reply with a NO response if attempted. Such a + response SHOULD contain the ACTIVE response code. If a client wishes + to delete an active script, it should use the SETACTIVE command to + disable the script first. + + Example: + + C: Deletescript "foo" + S: Ok + + C: Deletescript "baz" + S: No (ACTIVE) "You may not delete an active script" + + + + + + + + + + +Melnikov & Martin Standards Track [Page 25] + +RFC 5804 ManageSieve July 2010 + + +2.11. RENAMESCRIPT Command + + Arguments: String - Old Script name + String - New Script name + + This command is used to rename a user's Sieve script. Servers MUST + reply with a NO response if the old script does not exist (in which + case the NONEXISTENT response code SHOULD be included), or a script + with the new name already exists (in which case the ALREADYEXISTS + response code SHOULD be included). Renaming the active script is + allowed; the renamed script remains active. + + Example: + + C: Renamescript "foo" "bar" + S: Ok + + C: Renamescript "baz" "bar" + S: No "bar already exists" + + If the server doesn't support the RENAMESCRIPT command, the client + can emulate it by performing the following steps: + + 1. List available scripts with LISTSCRIPTS. If the script with the + new script name exists, then the client should ask the user + whether to abort the operation, to replace the script (by issuing + the DELETESCRIPT <newname> after that), or to choose a different + name. + + 2. Download the old script with GETSCRIPT <oldname>. + + 3. Upload the old script with the new name: PUTSCRIPT <newname>. + + 4. If the old script was active (as reported by LISTSCRIPTS in step + 1), then make the new script active: SETACTIVE <newname>. + + 5. Delete the old script: DELETESCRIPT <oldname>. + + Note that these steps don't describe how to handle various other + error conditions (for example, NO response containing QUOTA response + code in step 3). Error handling is left as an exercise for the + reader. + + + + + + + + + +Melnikov & Martin Standards Track [Page 26] + +RFC 5804 ManageSieve July 2010 + + +2.12. CHECKSCRIPT Command + + Arguments: String - Script content + + The CHECKSCRIPT command is used by the client to verify Sieve script + validity without storing the script on the server. + + The server MUST check the submitted script for syntactic validity, + which includes checking that all Sieve extensions mentioned in Sieve + script "require" statement(s) are supported by the Sieve interpreter. + (Note that if the Sieve interpreter supports the Sieve "ihave" + extension [I-HAVE], any unrecognized/unsupported extension mentioned + in the "ihave" test MUST NOT cause the syntactic validation failure.) + If the script fails this test, the server MUST reply with a NO + response. The message given with a NO response MUST be human + readable and SHOULD contain a specific error message giving the line + number of the first error. Implementors should strive to produce + helpful error messages similar to those given by programming language + compilers. Client implementations should note that this may be a + multiline literal string with more than one error message separated + by CRLFs. The human-readable message is in the language returned in + the latest LANGUAGE capability (or in "i-default"; see Section 1.7), + encoded in UTF-8 [UTF-8]. + + Examples: + + C: CheckScript {31+} + C: #comment + C: InvalidSieveCommand + C: + S: NO "line 2: Syntax error" + + A ManageSieve server supporting this command MUST NOT check if the + script will put the current user over its quota limit. + + An OK response MAY contain the WARNINGS response code. In such a + case, the human-readable message that follows the OK response SHOULD + contain a specific warning message (or messages) giving the line + number(s) in the script that might contain errors not intended by the + script writer. The human-readable message is in the language + returned in the latest LANGUAGE capability (or in "i-default"; see + Section 1.7), encoded in UTF-8 [UTF-8]. A client seeing such a + response code SHOULD present the message to the user. + + + + + + + + +Melnikov & Martin Standards Track [Page 27] + +RFC 5804 ManageSieve July 2010 + + +2.13. NOOP Command + + Arguments: String - tag to echo back (optional) + + The NOOP command does nothing, beyond returning a response to the + client. It may be used by clients for protocol re-synchronization or + to reset any inactivity auto-logout timer on the server. + + The response to the NOOP command is always OK, followed by the TAG + response code together with the supplied string. If no string was + supplied in the NOOP command, the TAG response code MUST NOT be + included. + + Examples: + + C: NOOP + S: OK "NOOP completed" + + C: NOOP "STARTTLS-SYNC-42" + S: OK (TAG {16} + S: STARTTLS-SYNC-42) "Done" + +2.14. Recommended Extensions + + The UNAUTHENTICATE extension (advertised as the "UNAUTHENTICATE" + capability with no parameters) defines a new UNAUTHENTICATE command, + which allows a client to return the server to non-authenticated + state. Support for this extension is RECOMMENDED. + +2.14.1. UNAUTHENTICATE Command + + The UNAUTHENTICATE command returns the server to the + non-authenticated state. It doesn't affect any previously + established TLS [TLS] or SASL (Section 2.1) security layer. + + The UNAUTHENTICATE command is only valid in authenticated state. If + issued in a wrong state, the server MUST reject it with a NO + response. + + The UNAUTHENTICATE command has no parameters. + + When issued in the authenticated state, the UNAUTHENTICATE command + MUST NOT fail (i.e., it must never return anything other than OK or + BYE). + + + + + + + +Melnikov & Martin Standards Track [Page 28] + +RFC 5804 ManageSieve July 2010 + + +3. Sieve URL Scheme + + URI scheme name: sieve + + Status: permanent + + URI scheme syntax: Described using ABNF [ABNF]. Some ABNF + productions not defined below are from [URI-GEN]. + + sieveurl = sieveurl-server / sieveurl-list-scripts / + sieveurl-script + + sieveurl-server = "sieve://" authority + + sieveurl-list-scripts = "sieve://" authority ["/"] + + sieveurl-script = "sieve://" authority "/" + [owner "/"] scriptname + + authority = <defined in [URI-GEN]> + + owner = *ochar + ;; %-encoded version of [SASL] authorization + ;; identity (script owner) or "userid". + ;; + ;; Empty owner is used to reference + ;; global scripts. + ;; + ;; Note that ASCII characters such as " ", ";", + ;; "&", "=", "/" and "?" must be %-encoded + ;; as per rule specified in [URI-GEN]. + + scriptname = 1*ochar + ;; %-encoded version of UTF-8 representation + ;; of the script name. + ;; Note that ASCII characters such as " ", ";", + ;; "&", "=", "/" and "?" must be %-encoded + ;; as per rule specified in [URI-GEN]. + + ochar = unreserved / pct-encoded / sub-delims-sh / + ":" / "@" + ;; Same as [URI-GEN] 'pchar', + ;; but without ";", "&" and "=". + + unreserved = <defined in [URI-GEN]> + + pct-encoded = <defined in [URI-GEN]> + + + + +Melnikov & Martin Standards Track [Page 29] + +RFC 5804 ManageSieve July 2010 + + + sub-delims-sh = "!" / "$" / "'" / "(" / ")" / + "*" / "+" / "," + ;; Same as [URI-GEN] sub-delims, + ;; but without ";", "&" and "=". + + URI scheme semantics: + + A Sieve URL identifies a Sieve server or a Sieve script on a Sieve + server. The latter form is associated with the application/sieve + MIME type defined in [SIEVE]. There is no MIME type associated + with the former form of Sieve URI. + + The server form is used in the REFERRAL response code (see Section + 1.3) in order to designate another server where the client should + perform its operations. + + The script form allows to retrieve (GETSCRIPT), update + (PUTSCRIPT), delete (DELETESCRIPT), or activate (SETACTIVE) the + named script; however, the most typical action would be to + retrieve the script. If the script name is empty (omitted), the + URI requests that the client lists available scripts using the + LISTSCRIPTS command. + + Encoding considerations: + + The script name and/or the owner, if present, is in UTF-8. Non-- + US-ASCII UTF-8 octets MUST be percent-encoded as described in + [URI-GEN]. US-ASCII characters such as " " (space), ";", "&", + "=", "/" and "?" MUST be %-encoded as described in [URI-GEN]. + Note that "&" and "?" are in this list in order to allow for + future extensions. + + Note that the empty owner (e.g., sieve://example.com//script) is + different from the missing owner (e.g., + sieve://example.com/script) and is reserved for referencing global + scripts. + + The user name (in the "authority" part), if present, is in UTF-8. + Non-US-ASCII UTF-8 octets MUST be percent-encoded as described in + [URI-GEN]. + + Applications/protocols that use this URI scheme name: + ManageSieve [RFC5804] clients and servers. Clients that can store + user preferences in protocols such as [LDAP] or [ACAP]. + + Interoperability considerations: None. + + + + + +Melnikov & Martin Standards Track [Page 30] + +RFC 5804 ManageSieve July 2010 + + + Security considerations: + The <scriptname> part of a ManageSieve URL might potentially disclose + some confidential information about the author of the script or, + depending on a ManageSieve implementation, about configuration of the + mail system. The latter might be used to prepare for a more complex + attack on the mail system. + + Clients resolving ManageSieve URLs that wish to achieve data + confidentiality and/or integrity SHOULD use the STARTTLS command (if + supported by the server) before starting authentication, or use a + SASL mechanism, such as GSSAPI, that provides a confidentiality + security layer. + + Contact: Alexey Melnikov <alexey.melnikov@isode.com> + + Author/Change controller: IESG. + + References: This document and RFC 5228 [SIEVE]. + +4. Formal Syntax + + The following syntax specification uses the Augmented Backus-Naur + Form (BNF) notation as specified in [ABNF]. This uses the ABNF core + rules as specified in Appendix A of the ABNF specification [ABNF]. + "UTF8-2", "UTF8-3", and "UTF8-4" non-terminal are defined in [UTF-8]. + + Except as noted otherwise, all alphabetic characters are case- + insensitive. The use of upper- or lowercase characters to define + token strings is for editorial clarity only. Implementations MUST + accept these strings in a case-insensitive fashion. + + SAFE-CHAR = %x01-09 / %x0B-0C / %x0E-21 / %x23-5B / + %x5D-7F + ;; any TEXT-CHAR except QUOTED-SPECIALS + + QUOTED-CHAR = SAFE-UTF8-CHAR / "\" QUOTED-SPECIALS + + QUOTED-SPECIALS = DQUOTE / "\" + + SAFE-UTF8-CHAR = SAFE-CHAR / UTF8-2 / UTF8-3 / UTF8-4 + ;; <UTF8-2>, <UTF8-3>, and <UTF8-4> + ;; are defined in [UTF-8]. + + ATOM-CHAR = "!" / %x23-27 / %x2A-5B / %x5D-7A / %x7C-7E + ;; Any CHAR except ATOM-SPECIALS + + ATOM-SPECIALS = "(" / ")" / "{" / SP / CTL / QUOTED-SPECIALS + + + + +Melnikov & Martin Standards Track [Page 31] + +RFC 5804 ManageSieve July 2010 + + + NZDIGIT = %x31-39 + ;; 1-9 + + atom = 1*1024ATOM-CHAR + + iana-token = atom + ;; MUST be registered with IANA + + auth-type = DQUOTE auth-type-name DQUOTE + + auth-type-name = iana-token + ;; as defined in SASL [SASL] + + command = (command-any / command-auth / + command-nonauth) CRLF + ;; Modal based on state + + command-any = command-capability / command-logout / + command-noop + ;; Valid in all states + + command-auth = command-getscript / command-setactive / + command-listscripts / command-deletescript / + command-putscript / command-checkscript / + command-havespace / + command-renamescript / + command-unauthenticate + ;; Valid only in Authenticated state + + command-nonauth = command-authenticate / command-starttls + ;; Valid only when in Non-Authenticated + ;; state + + command-authenticate = "AUTHENTICATE" SP auth-type [SP string] + *(CRLF string) + + command-capability = "CAPABILITY" + + command-deletescript = "DELETESCRIPT" SP sieve-name + + command-getscript = "GETSCRIPT" SP sieve-name + + command-havespace = "HAVESPACE" SP sieve-name SP number + + command-listscripts = "LISTSCRIPTS" + + command-noop = "NOOP" [SP string] + + + + +Melnikov & Martin Standards Track [Page 32] + +RFC 5804 ManageSieve July 2010 + + + command-logout = "LOGOUT" + + command-putscript = "PUTSCRIPT" SP sieve-name SP sieve-script + + command-checkscript = "CHECKSCRIPT" SP sieve-script + + sieve-script = string + + command-renamescript = "RENAMESCRIPT" SP old-sieve-name SP + new-sieve-name + + old-sieve-name = sieve-name + + new-sieve-name = sieve-name + + command-setactive = "SETACTIVE" SP active-sieve-name + + command-starttls = "STARTTLS" + + command-unauthenticate= "UNAUTHENTICATE" + + extend-token = atom + ;; MUST be defined by a Standards Track or + ;; IESG-approved experimental protocol + ;; extension + + extension-data = extension-item *(SP extension-item) + + extension-item = extend-token / string / number / + "(" [extension-data] ")" + + literal-c2s = "{" number "+}" CRLF *OCTET + ;; The number represents the number of + ;; octets. + ;; This type of literal can only be sent + ;; from the client to the server. + + literal-s2c = "{" number "}" CRLF *OCTET + ;; Almost identical to literal-c2s, + ;; but with no '+' character. + ;; The number represents the number of + ;; octets. + ;; This type of literal can only be sent + ;; from the server to the client. + + + + + + + +Melnikov & Martin Standards Track [Page 33] + +RFC 5804 ManageSieve July 2010 + + + number = (NZDIGIT *DIGIT) / "0" + ;; A 32-bit unsigned number + ;; with no extra leading zeros. + ;; (0 <= n < 4,294,967,296) + + number-str = string + ;; <number> encoded as a <string>. + + quoted = DQUOTE *1024QUOTED-CHAR DQUOTE + ;; limited to 1024 octets between the <">s + + resp-code = "AUTH-TOO-WEAK" / "ENCRYPT-NEEDED" / "QUOTA" + ["/" ("MAXSCRIPTS" / "MAXSIZE")] / + resp-code-sasl / + resp-code-referral / + "TRANSITION-NEEDED" / "TRYLATER" / + "ACTIVE" / "NONEXISTENT" / + "ALREADYEXISTS" / "WARNINGS" / + "TAG" SP string / + resp-code-ext + + resp-code-referral = "REFERRAL" SP sieveurl + + resp-code-sasl = "SASL" SP string + + resp-code-name = iana-token + ;; The response code name is hierarchical, + ;; separated by '/'. + ;; The response code name MUST NOT start + ;; with '/'. + + resp-code-ext = resp-code-name [SP extension-data] + ;; unknown response codes MUST be tolerated + ;; by the client. + + response = response-authenticate / + response-logout / + response-getscript / + response-setactive / + response-listscripts / + response-deletescript / + response-putscript / + response-checkscript / + response-capability / + response-havespace / + response-starttls / + response-renamescript / + response-noop / + + + +Melnikov & Martin Standards Track [Page 34] + +RFC 5804 ManageSieve July 2010 + + + response-unauthenticate + + response-authenticate = *(string CRLF) + ((response-ok [response-capability]) / + response-nobye) + ;; <response-capability> is REQUIRED if a + ;; SASL security layer was negotiated and + ;; MUST be omitted otherwise. + + response-capability = *(single-capability) response-oknobye + + single-capability = capability-name [SP string] CRLF + + capability-name = string + + ;; Note that literal-s2c is allowed. + + initial-capabilities = DQUOTE "IMPLEMENTATION" DQUOTE SP string / + DQUOTE "SASL" DQUOTE SP sasl-mechs / + DQUOTE "SIEVE" DQUOTE SP sieve-extensions / + DQUOTE "MAXREDIRECTS" DQUOTE SP number-str / + DQUOTE "NOTIFY" DQUOTE SP notify-mechs / + DQUOTE "STARTTLS" DQUOTE / + DQUOTE "LANGUAGE" DQUOTE SP language / + DQUOTE "VERSION" DQUOTE SP version / + DQUOTE "OWNER" DQUOTE SP string + ;; Each capability conforms to + ;; the syntax for single-capability. + ;; Also, note that the capability name + ;; can be returned as either literal-s2c + ;; or quoted, even though only "quoted" + ;; string is shown above. + + version = ( DQUOTE "1.0" DQUOTE ) / version-ext + + version-ext = DQUOTE ver-major "." ver-minor DQUOTE + ; Future versions specified in updates + ; to this document. An increment to + ; the ver-major means a backward-incompatible + ; change to the protocol, e.g., "3.5" (ver-major "3") + ; is not backward-compatible with any "2.X" version. + ; Any version "Z.W" MUST be backward compatible + ; with any version "Z.Q", where Q < W. + ; For example, version "2.4" is backward compatible + ; with version "2.0", "2.1", "2.2", and "2.3". + + ver-major = number + + + + +Melnikov & Martin Standards Track [Page 35] + +RFC 5804 ManageSieve July 2010 + + + ver-minor = number + + sasl-mechs = string + ; Space-separated list of SASL mechanisms, + ; each SASL mechanism name complies with rules + ; specified in [SASL]. + ; Can be empty. + + sieve-extensions = string + ; Space-separated list of supported SIEVE extensions. + ; Can be empty. + + language = string + ; Contains <Language-Tag> from [RFC5646]. + + + notify-mechs = string + ; Space-separated list of URI schema parts + ; for supported notification [NOTIFY] methods. + ; MUST NOT be empty. + + response-deletescript = response-oknobye + + response-getscript = (sieve-script CRLF response-ok) / + response-nobye + + response-havespace = response-oknobye + + response-listscripts = *(sieve-name [SP "ACTIVE"] CRLF) + response-oknobye + ;; ACTIVE may only occur with one sieve-name + + response-logout = response-oknobye + + response-unauthenticate= response-oknobye + ;; "NO" response can only be returned when + ;; the command is issued in a wrong state + ;; or has a wrong number of parameters + + response-ok = "OK" [SP "(" resp-code ")"] + [SP string] CRLF + ;; The string contains human-readable text + ;; encoded as UTF-8. + + response-nobye = ("NO" / "BYE") [SP "(" resp-code ")"] + [SP string] CRLF + ;; The string contains human-readable text + ;; encoded as UTF-8. + + + +Melnikov & Martin Standards Track [Page 36] + +RFC 5804 ManageSieve July 2010 + + + response-oknobye = response-ok / response-nobye + + response-noop = response-ok + + response-putscript = response-oknobye + + response-checkscript = response-oknobye + + response-renamescript = response-oknobye + + response-setactive = response-oknobye + + response-starttls = (response-ok response-capability) / + response-nobye + + sieve-name = string + ;; See Section 1.6 for the full list of + ;; prohibited characters. + ;; Empty string is not allowed. + + active-sieve-name = string + ;; See Section 1.6 for the full list of + ;; prohibited characters. + ;; This is similar to <sieve-name>, but + ;; empty string is allowed and has a special + ;; meaning. + + string = quoted / literal-c2s / literal-s2c + ;; literal-c2s is only allowed when sent + ;; from the client to the server. + ;; literal-s2c is only allowed when sent + ;; from the server to the client. + ;; quoted is allowed in either direction. + +5. Security Considerations + + The AUTHENTICATE command uses SASL [SASL] to provide authentication + and authorization services. Integrity and privacy services can be + provided by [SASL] and/or [TLS]. When a SASL mechanism is used, the + security considerations for that mechanism apply. + + This protocol's transactions are susceptible to passive observers or + man-in-the-middle attacks that alter the data, unless the optional + encryption and integrity services of the SASL (via the AUTHENTICATE + command) and/or [TLS] (via the STARTTLS command) are enabled, or an + external security mechanism is used for protection. It may be useful + to allow configuration of both clients and servers to refuse to + transfer sensitive information in the absence of strong encryption. + + + +Melnikov & Martin Standards Track [Page 37] + +RFC 5804 ManageSieve July 2010 + + + If an implementation supports SASL mechanisms that are vulnerable to + passive eavesdropping attacks (such as [PLAIN]), then the + implementation MUST support at least one configuration where these + SASL mechanisms are not advertised or used without the presence of an + external security layer such as [TLS]. + + Some response codes returned on failed AUTHENTICATE command may + disclose whether or not the username is valid (e.g., TRANSITION- + NEEDED), so server implementations SHOULD provide the ability to + disable these features (or make them not conditional on a per-user + basis) for sites concerned about such disclosure. In the case of + ENCRYPT-NEEDED, if it is applied to all identities then no extra + information is disclosed, but if it is applied on a per-user basis it + can disclose information. + + A compromised or malicious server can use the TRANSITION-NEEDED + response code to force the client that is configured to use a + mechanism that does not disclose the user's password to the server + (e.g., Kerberos), to send the bare password to the server. Clients + SHOULD have the ability to disable the password transition feature, + or disclose that risk to the user and offer the user an option of how + to proceed. + +6. IANA Considerations + + IANA has reserved TCP port number 4190 for use with the ManageSieve + protocol described in this document. + + IANA has registered the "sieve" URI scheme defined in Section 3 of + this document. + + IANA has registered "sieve" in the "GSSAPI/Kerberos/SASL Service + Names" registry. + + IANA has created a new registry for ManageSieve capabilities. The + registration template for ManageSieve capabilities is specified in + Section 6.1. ManageSieve protocol capabilities MUST be specified in + a Standards-Track or IESG-approved Experimental RFC. + + IANA has created a new registry for ManageSieve response codes. The + registration template for ManageSieve response codes is specified in + Section 6.3. ManageSieve protocol response codes MUST be specified + in a Standards-Track or IESG-approved Experimental RFC. + + + + + + + + +Melnikov & Martin Standards Track [Page 38] + +RFC 5804 ManageSieve July 2010 + + +6.1. ManageSieve Capability Registration Template + + To: iana@iana.org + Subject: ManageSieve Capability Registration + + Please register the following ManageSieve capability: + + Capability name: + Description: + Relevant publications: + Person & email address to contact for further information: + Author/Change controller: + +6.2. Registration of Initial ManageSieve Capabilities + + To: iana@iana.org + Subject: ManageSieve Capability Registration + + Please register the following ManageSieve capabilities: + + Capability name: IMPLEMENTATION + Description: Its value contains the name of the server + implementation and its version. + Relevant publications: this RFC, Section 1.7. + Person & email address to contact for further information: + Alexey Melnikov <alexey.melnikov@isode.com> + Author/Change controller: IESG. + + Capability name: SASL + Description: Its value contains a space-separated list of SASL + mechanisms supported by the server. + Relevant publications: this RFC, Sections 1.7 and 2.1. + Person & email address to contact for further information: + Alexey Melnikov <alexey.melnikov@isode.com> + Author/Change controller: IESG. + + Capability name: SIEVE + Description: Its value contains a space-separated list of supported + SIEVE extensions. + Relevant publications: this RFC, Section 1.7. Also [SIEVE]. + Person & email address to contact for further information: + Alexey Melnikov <alexey.melnikov@isode.com> + Author/Change controller: IESG. + + + + + + + + +Melnikov & Martin Standards Track [Page 39] + +RFC 5804 ManageSieve July 2010 + + + Capability name: STARTTLS + Description: This capability is returned if the server supports TLS + (STARTTLS command). + Relevant publications: this RFC, Sections 1.7 and 2.2. + Person & email address to contact for further information: + Alexey Melnikov <alexey.melnikov@isode.com> + Author/Change controller: IESG. + + Capability name: NOTIFY + Description: This capability is returned if the server supports the + 'enotify' [NOTIFY] Sieve extension. + Relevant publications: this RFC, Section 1.7. + Person & email address to contact for further information: + Alexey Melnikov <alexey.melnikov@isode.com> + Author/Change controller: IESG. + + Capability name: MAXREDIRECTS + Description: This capability returns the limit on the number of + Sieve "redirect" actions a script can perform during a + single evaluation. The value is a non-negative number + represented as a ManageSieve string. + Relevant publications: this RFC, Section 1.7. + Person & email address to contact for further information: + Alexey Melnikov <alexey.melnikov@isode.com> + Author/Change controller: IESG. + + Capability name: LANGUAGE + Description: The language (<Language-Tag> from [RFC5646]) currently + used for human-readable error messages. + Relevant publications: this RFC, Section 1.7. + Person & email address to contact for further information: + Alexey Melnikov <alexey.melnikov@isode.com> + Author/Change controller: IESG. + + Capability name: OWNER + Description: Its value contains the UTF-8-encoded name of the + currently logged-in user ("authorization identity" + according to RFC 4422). + Relevant publications: this RFC, Section 1.7. + Person & email address to contact for further information: + Alexey Melnikov <alexey.melnikov@isode.com> + Author/Change controller: IESG. + + + + + + + + + +Melnikov & Martin Standards Track [Page 40] + +RFC 5804 ManageSieve July 2010 + + + Capability name: VERSION + Description: This capability is returned if the server is compliant + with RFC 5804; i.e., that it supports RENAMESCRIPT, + CHECKSCRIPT, and NOOP commands. + Relevant publications: this RFC, Sections 2.11, 2.12, and 2.13. + Person & email address to contact for further information: + Alexey Melnikov <alexey.melnikov@isode.com> + Author/Change controller: IESG. + +6.3. ManageSieve Response Code Registration Template + + To: iana@iana.org + Subject: ManageSieve Response Code Registration + + Please register the following ManageSieve response code: + + Response Code: + Arguments (use ABNF to specify syntax, or the word NONE if none + can be specified): + Purpose: + Published Specification(s): + Person & email address to contact for further information: + Author/Change controller: + +6.4. Registration of Initial ManageSieve Response Codes + + To: iana@iana.org + Subject: ManageSieve Response Code Registration + + Please register the following ManageSieve response codes: + + Response Code: AUTH-TOO-WEAK + Arguments (use ABNF to specify syntax, or the word NONE if none can + be specified): NONE + Purpose: This response code is returned in the NO response from + an AUTHENTICATE command. It indicates that site + security policy forbids the use of the requested + mechanism for the specified authentication identity. + Published Specification(s): [RFC5804] + Person & email address to contact for further information: + Alexey Melnikov <alexey.melnikov@isode.com> + Author/Change controller: IESG. + + + + + + + + + +Melnikov & Martin Standards Track [Page 41] + +RFC 5804 ManageSieve July 2010 + + + Response Code: ENCRYPT-NEEDED + Arguments (use ABNF to specify syntax, or the word NONE if none can + be specified): NONE + Purpose: This response code is returned in the NO response from + an AUTHENTICATE command. It indicates that site + security policy requires the use of a strong + encryption mechanism for the specified authentication + identity and mechanism. + Published Specification(s): [RFC5804] + Person & email address to contact for further information: + Alexey Melnikov <alexey.melnikov@isode.com> + Author/Change controller: IESG. + + Response Code: QUOTA + Arguments (use ABNF to specify syntax, or the word NONE if none can + be specified): NONE + Purpose: If this response code is returned in the NO/BYE + response, it means that the command would have placed + the user above the site-defined quota constraints. If + this response code is returned in the OK response, it + can mean that the user is near its quota or that the + user exceeded its quota, but the server supports soft + quotas. + Published Specification(s): [RFC5804] + Person & email address to contact for further information: + Alexey Melnikov <alexey.melnikov@isode.com> + Author/Change controller: IESG. + + Response Code: QUOTA/MAXSCRIPTS + Arguments (use ABNF to specify syntax, or the word NONE if none can + be specified): NONE + Purpose: If this response code is returned in the NO/BYE + response, it means that the command would have placed + the user above the site-defined limit on the number of + Sieve scripts. If this response code is returned in + the OK response, it can mean that the user is near its + quota or that the user exceeded its quota, but the + server supports soft quotas. This response code is a + more specific version of the QUOTA response code. + Published Specification(s): [RFC5804] + Person & email address to contact for further information: + Alexey Melnikov <alexey.melnikov@isode.com> + Author/Change controller: IESG. + + + + + + + + +Melnikov & Martin Standards Track [Page 42] + +RFC 5804 ManageSieve July 2010 + + + Response Code: QUOTA/MAXSIZE + Arguments (use ABNF to specify syntax, or the word NONE if none can + be specified): NONE + Purpose: If this response code is returned in the NO/BYE + response, it means that the command would have placed + the user above the site-defined maximum script size. + If this response code is returned in the OK response, + it can mean that the user is near its quota or that + the user exceeded its quota, but the server supports + soft quotas. This response code is a more specific + version of the QUOTA response code. + Published Specification(s): [RFC5804] + Person & email address to contact for further information: + Alexey Melnikov <alexey.melnikov@isode.com> + Author/Change controller: IESG. + + Response Code: REFERRAL + Arguments (use ABNF to specify syntax, or the word NONE if none can + be specified): <sieveurl> + Purpose: This response code may be returned with a BYE result + from any command, and includes a mandatory parameter + that indicates what server to access to manage this + user's Sieve scripts. The server will be specified by + a Sieve URL (see Section 3). The scriptname portion + of the URL MUST NOT be specified. The client should + authenticate to the specified server and use it for + all further commands in the current session. + Published Specification(s): [RFC5804] + Person & email address to contact for further information: + Alexey Melnikov <alexey.melnikov@isode.com> + Author/Change controller: IESG. + + Response Code: SASL + Arguments (use ABNF to specify syntax, or the word NONE if none can + be specified): <string> + Purpose: This response code can occur in the OK response to a + successful AUTHENTICATE command and includes the + optional final server response data from the server as + specified by [SASL]. + Published Specification(s): [RFC5804] + Person & email address to contact for further information: + Alexey Melnikov <alexey.melnikov@isode.com> + Author/Change controller: IESG. + + + + + + + + +Melnikov & Martin Standards Track [Page 43] + +RFC 5804 ManageSieve July 2010 + + + Response Code: TRANSITION-NEEDED + Arguments (use ABNF to specify syntax, or the word NONE if none can + be specified): NONE + Purpose: This response code occurs in a NO response of an + AUTHENTICATE command. It indicates that the user name + is valid, but the entry in the authentication database + needs to be updated in order to permit authentication + with the specified mechanism. This is typically done + by establishing a secure channel using TLS, followed + by authenticating once using the [PLAIN] + authentication mechanism. The selected mechanism + SHOULD then work for authentications in subsequent + sessions. + Published Specification(s): [RFC5804] + Person & email address to contact for further information: + Alexey Melnikov <alexey.melnikov@isode.com> + Author/Change controller: IESG. + + Response Code: TRYLATER + Arguments (use ABNF to specify syntax, or the word NONE if none can + be specified): NONE + Purpose: A command failed due to a temporary server failure. + The client MAY continue using local information and + try the command later. This response code only make + sense when returned in a NO/BYE response. + Published Specification(s): [RFC5804] + Person & email address to contact for further information: + Alexey Melnikov <alexey.melnikov@isode.com> + Author/Change controller: IESG. + + Response Code: ACTIVE + Arguments (use ABNF to specify syntax, or the word NONE if none can + be specified): NONE + Purpose: A command failed because it is not allowed on the + active script, for example, DELETESCRIPT on the active + script. This response code only makes sense when + returned in a NO/BYE response. + Published Specification(s): [RFC5804] + Person & email address to contact for further information: + Alexey Melnikov <alexey.melnikov@isode.com> + Author/Change controller: IESG. + + + + + + + + + + +Melnikov & Martin Standards Track [Page 44] + +RFC 5804 ManageSieve July 2010 + + + Response Code: NONEXISTENT + Arguments (use ABNF to specify syntax, or the word NONE if none can + be specified): NONE + Purpose: A command failed because the referenced script name + doesn't exist. This response code only makes sense + when returned in a NO/BYE response. + Published Specification(s): [RFC5804] + Person & email address to contact for further information: + Alexey Melnikov <alexey.melnikov@isode.com> + Author/Change controller: IESG. + + Response Code: ALREADYEXISTS + Arguments (use ABNF to specify syntax, or the word NONE if none can + be specified): NONE + Purpose: A command failed because the referenced script name + already exists. This response code only makes sense + when returned in a NO/BYE response. + Published Specification(s): [RFC5804] + Person & email address to contact for further information: + Alexey Melnikov <alexey.melnikov@isode.com> + Author/Change controller: IESG. + + Response Code: WARNINGS + Arguments (use ABNF to specify syntax, or the word NONE if none can + be specified): NONE + Purpose: This response code MAY be returned by the server in + the OK response (but it might be returned with the NO/ + BYE response as well) and signals the client that even + though the script is syntactically valid, it might + contain errors not intended by the script writer. + Published Specification(s): [RFC5804] + Person & email address to contact for further information: + Alexey Melnikov <alexey.melnikov@isode.com> + Author/Change controller: IESG. + + Response Code: TAG + Arguments (use ABNF to specify syntax, or the word NONE if none can + be specified): string + Purpose: This response code name is followed by a string + specified in the command that caused this response. + It is typically used for client state synchronization. + Published Specification(s): [RFC5804] + Person & email address to contact for further information: + Alexey Melnikov <alexey.melnikov@isode.com> + Author/Change controller: IESG. + + + + + + +Melnikov & Martin Standards Track [Page 45] + +RFC 5804 ManageSieve July 2010 + + +7. Internationalization Considerations + + The LANGUAGE capability (see Section 1.7) allows a client to discover + the current language used in all human-readable responses that might + be returned at the end of any OK/NO/BYE response. Human-readable + text in OK responses typically doesn't need to be shown to the user, + unless it is returned in response to a PUTSCRIPT or CHECKSCRIPT + command that also contains the WARNINGS response code (Section 1.3). + Human-readable text from NO/BYE responses is intended be shown to the + user, unless the client can automatically handle failure of the + command that caused such a response. Clients SHOULD use response + codes (Section 1.3) for automatic error handling. Response codes MAY + also be used by the client to present error messages in a language + understood by the user, for example, if the LANGUAGE capability + doesn't return a language understood by the user. + + Note that the human-readable text from OK (WARNINGS) or NO/BYE + responses for PUTSCRIPT/CHECKSCRIPT commands is intended for advanced + users that understand Sieve language. Such advanced users are often + sophisticated enough to be able to handle whatever language the + server is using, even if it is not their preferred language, and will + want to see error/warning text no matter what language the server + puts it in. + + A client that generates Sieve script automatically, for example, if + the script is generated without user intervention or from a UI that + presents an abstract list of conditions and corresponding actions, + SHOULD NOT present warning/error messages to the user, because the + user might not even be aware that the client is using Sieve + underneath. However, if the client has a debugging mode, such + warnings/errors SHOULD be available in the debugging mode. + + Note that this document doesn't provide a way to modify the currently + used language. It is expected that a future extension will address + that. + +8. Acknowledgements + + Thanks to Simon Josefsson, Larry Greenfield, Allen Johnson, Chris + Newman, Lyndon Nerenberg, Tim Showalter, Sarah Robeson, Walter Wong, + Barry Leiba, Arnt Gulbrandsen, Stephan Bosch, Ken Murchison, Phil + Pennock, Ned Freed, Jeffrey Hutzelman, Mark E. Mallett, Dilyan + Palauzov, Dave Cridland, Aaron Stone, Robert Burrell Donkin, Patrick + Ben Koetter, Bjoern Hoehrmann, Martin Duerst, Pasi Eronen, Magnus + Westerlund, Tim Polk, and Julien Coloos for help with this document. + Special thank you to Phil Pennock for providing text for the NOOP + command, as well as finding various bugs in the document. + + + + +Melnikov & Martin Standards Track [Page 46] + +RFC 5804 ManageSieve July 2010 + + +9. References + +9.1. Normative References + + [ABNF] Crocker, D. and P. Overell, "Augmented BNF for Syntax + Specifications: ABNF", STD 68, RFC 5234, January 2008. + + [ACAP] Newman, C. and J. Myers, "ACAP -- Application + Configuration Access Protocol", RFC 2244, November + 1997. + + [BASE64] Josefsson, S., "The Base16, Base32, and Base64 Data + Encodings", RFC 4648, October 2006. + + [DNS-SRV] Gulbrandsen, A., Vixie, P., and L. Esibov, "A DNS RR + for specifying the location of services (DNS SRV)", + RFC 2782, February 2000. + + [KEYWORDS] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, March 1997. + + [NET-UNICODE] Klensin, J. and M. Padlipsky, "Unicode Format for + Network Interchange", RFC 5198, March 2008. + + [NOTIFY] Melnikov, A., Leiba, B., Segmuller, W., and T. Martin, + "Sieve Email Filtering: Extension for Notifications", + RFC 5435, January 2009. + + [RFC2277] Alvestrand, H., "IETF Policy on Character Sets and + Languages", BCP 18, RFC 2277, January 1998. + + [RFC2460] Deering, S. and R. Hinden, "Internet Protocol, Version + 6 (IPv6) Specification", RFC 2460, December 1998. + + [RFC3490] Faltstrom, P., Hoffman, P., and A. Costello, + "Internationalizing Domain Names in Applications + (IDNA)", RFC 3490, March 2003. + + [RFC4519] Sciberras, A., "Lightweight Directory Access Protocol + (LDAP): Schema for User Applications", RFC 4519, June + 2006. + + [RFC5646] Phillips, A. and M. Davis, "Tags for Identifying + Languages", BCP 47, RFC 5646, September 2009. + + [RFC791] Postel, J., "Internet Protocol", STD 5, RFC 791, + September 1981. + + + + +Melnikov & Martin Standards Track [Page 47] + +RFC 5804 ManageSieve July 2010 + + + [SASL] Melnikov, A. and K. Zeilenga, "Simple Authentication + and Security Layer (SASL)", RFC 4422, June 2006. + + [SASLprep] Zeilenga, K., "SASLprep: Stringprep Profile for User + Names and Passwords", RFC 4013, February 2005. + + [SCRAM] Menon-Sen, A., Melnikov, A., Newman, C., and N. + Williams, "Salted Challenge Response Authentication + Mechanism (SCRAM) SASL and GSS-API Mechanisms", RFC + 5802, July 2010. + + [SIEVE] Guenther, P. and T. Showalter, "Sieve: An Email + Filtering Language", RFC 5228, January 2008. + + [StringPrep] Hoffman, P. and M. Blanchet, "Preparation of + Internationalized Strings ("stringprep")", RFC 3454, + December 2002. + + [TLS] Dierks, T. and E. Rescorla, "The Transport Layer + Security (TLS) Protocol Version 1.2", RFC 5246, August + 2008. + + [URI-GEN] Berners-Lee, T., Fielding, R., and L. Masinter, + "Uniform Resource Identifier (URI): Generic Syntax", + STD 66, RFC 3986, January 2005. + + [UTF-8] Yergeau, F., "UTF-8, a transformation format of ISO + 10646", STD 63, RFC 3629, November 2003. + + [X509] Cooper, D., Santesson, S., Farrell, S., Boeyen, S., + Housley, R., and W. Polk, "Internet X.509 Public Key + Infrastructure Certificate and Certificate Revocation + List (CRL) Profile", RFC 5280, May 2008. + + [X509-SRV] Santesson, S., "Internet X.509 Public Key + Infrastructure Subject Alternative Name for Expression + of Service Name", RFC 4985, August 2007. + +9.2. Informative References + + [DIGEST-MD5] Leach, P. and C. Newman, "Using Digest Authentication + as a SASL Mechanism", RFC 2831, May 2000. + + [GSSAPI] Melnikov, A., "The Kerberos V5 ("GSSAPI") Simple + Authentication and Security Layer (SASL) Mechanism", + RFC 4752, November 2006. + + + + + +Melnikov & Martin Standards Track [Page 48] + +RFC 5804 ManageSieve July 2010 + + + [I-HAVE] Freed, N., "Sieve Email Filtering: Ihave Extension", + RFC 5463, March 2009. + + [IMAP] Crispin, M., "INTERNET MESSAGE ACCESS PROTOCOL - + VERSION 4rev1", RFC 3501, March 2003. + + [LDAP] Zeilenga, K., "Lightweight Directory Access Protocol + (LDAP): Technical Specification Road Map", RFC 4510, + June 2006. + + [PLAIN] Zeilenga, K., "The PLAIN Simple Authentication and + Security Layer (SASL) Mechanism", RFC 4616, August + 2006. + +Authors' Addresses + + Alexey Melnikov (editor) + Isode Limited + 5 Castle Business Village + 36 Station Road + Hampton, Middlesex TW12 2BX + UK + + EMail: Alexey.Melnikov@isode.com + + + Tim Martin + BeThereBeSquare, Inc. + 672 Haight st. + San Francisco, CA 94117 + USA + + Phone: +1 510 260-4175 + EMail: timmartin@alumni.cmu.edu + + + + + + + + + + + + + + + + + +Melnikov & Martin Standards Track [Page 49] + diff --git a/src/common/libManageSieve/doc/SASL CRAM/rfc2195.txt b/src/common/libManageSieve/doc/SASL CRAM/rfc2195.txt new file mode 100644 index 00000000..4a2725bf --- /dev/null +++ b/src/common/libManageSieve/doc/SASL CRAM/rfc2195.txt @@ -0,0 +1,283 @@ + + + + + + +Network Working Group J. Klensin +Request for Comments: 2195 R. Catoe +Category: Standards Track P. Krumviede +Obsoletes: 2095 MCI + September 1997 + + + IMAP/POP AUTHorize Extension for Simple Challenge/Response + +Status of this Memo + + This document specifies an Internet standards track protocol for the + Internet community, and requests discussion and suggestions for + improvements. Please refer to the current edition of the "Internet + Official Protocol Standards" (STD 1) for the standardization state + and status of this protocol. Distribution of this memo is unlimited. + +Abstract + + While IMAP4 supports a number of strong authentication mechanisms as + described in RFC 1731, it lacks any mechanism that neither passes + cleartext, reusable passwords across the network nor requires either + a significant security infrastructure or that the mail server update + a mail-system-wide user authentication file on each mail access. + This specification provides a simple challenge-response + authentication protocol that is suitable for use with IMAP4. Since + it utilizes Keyed-MD5 digests and does not require that the secret be + stored in the clear on the server, it may also constitute an + improvement on APOP for POP3 use as specified in RFC 1734. + +1. Introduction + + Existing Proposed Standards specify an AUTHENTICATE mechanism for the + IMAP4 protocol [IMAP, IMAP-AUTH] and a parallel AUTH mechanism for + the POP3 protocol [POP3-AUTH]. The AUTHENTICATE mechanism is + intended to be extensible; the four methods specified in [IMAP-AUTH] + are all fairly powerful and require some security infrastructure to + support. The base POP3 specification [POP3] also contains a + lightweight challenge-response mechanism called APOP. APOP is + associated with most of the risks associated with such protocols: in + particular, it requires that both the client and server machines have + access to the shared secret in cleartext form. CRAM offers a method + for avoiding such cleartext storage while retaining the algorithmic + simplicity of APOP in using only MD5, though in a "keyed" method. + + + + + + + +Klensin, Catoe & Krumviede Standards Track [Page 1] + +RFC 2195 IMAP/POP AUTHorize Extension September 1997 + + + At present, IMAP [IMAP] lacks any facility corresponding to APOP. + The only alternative to the strong mechanisms identified in [IMAP- + AUTH] is a presumably cleartext username and password, supported + through the LOGIN command in [IMAP]. This document describes a + simple challenge-response mechanism, similar to APOP and PPP CHAP + [PPP], that can be used with IMAP (and, in principle, with POP3). + + This mechanism also has the advantage over some possible alternatives + of not requiring that the server maintain information about email + "logins" on a per-login basis. While mechanisms that do require such + per-login history records may offer enhanced security, protocols such + as IMAP, which may have several connections between a given client + and server open more or less simultaneous, may make their + implementation particularly challenging. + +2. Challenge-Response Authentication Mechanism (CRAM) + + The authentication type associated with CRAM is "CRAM-MD5". + + The data encoded in the first ready response contains an + presumptively arbitrary string of random digits, a timestamp, and the + fully-qualified primary host name of the server. The syntax of the + unencoded form must correspond to that of an RFC 822 'msg-id' + [RFC822] as described in [POP3]. + + The client makes note of the data and then responds with a string + consisting of the user name, a space, and a 'digest'. The latter is + computed by applying the keyed MD5 algorithm from [KEYED-MD5] where + the key is a shared secret and the digested text is the timestamp + (including angle-brackets). + + This shared secret is a string known only to the client and server. + The `digest' parameter itself is a 16-octet value which is sent in + hexadecimal format, using lower-case ASCII characters. + + When the server receives this client response, it verifies the digest + provided. If the digest is correct, the server should consider the + client authenticated and respond appropriately. + + Keyed MD5 is chosen for this application because of the greater + security imparted to authentication of short messages. In addition, + the use of the techniques described in [KEYED-MD5] for precomputation + of intermediate results make it possible to avoid explicit cleartext + storage of the shared secret on the server system by instead storing + the intermediate results which are known as "contexts". + + + + + + +Klensin, Catoe & Krumviede Standards Track [Page 2] + +RFC 2195 IMAP/POP AUTHorize Extension September 1997 + + + CRAM does not support a protection mechanism. + + Example: + + The examples in this document show the use of the CRAM mechanism with + the IMAP4 AUTHENTICATE command [IMAP-AUTH]. The base64 encoding of + the challenges and responses is part of the IMAP4 AUTHENTICATE + command, not part of the CRAM specification itself. + + S: * OK IMAP4 Server + C: A0001 AUTHENTICATE CRAM-MD5 + S: + PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2UucmVzdG9uLm1jaS5uZXQ+ + C: dGltIGI5MTNhNjAyYzdlZGE3YTQ5NWI0ZTZlNzMzNGQzODkw + S: A0001 OK CRAM authentication successful + + In this example, the shared secret is the string + 'tanstaaftanstaaf'. Hence, the Keyed MD5 digest is produced by + calculating + + MD5((tanstaaftanstaaf XOR opad), + MD5((tanstaaftanstaaf XOR ipad), + <1896.697170952@postoffice.reston.mci.net>)) + + where ipad and opad are as defined in the keyed-MD5 Work in + Progress [KEYED-MD5] and the string shown in the challenge is the + base64 encoding of <1896.697170952@postoffice.reston.mci.net>. The + shared secret is null-padded to a length of 64 bytes. If the + shared secret is longer than 64 bytes, the MD5 digest of the + shared secret is used as a 16 byte input to the keyed MD5 + calculation. + + This produces a digest value (in hexadecimal) of + + b913a602c7eda7a495b4e6e7334d3890 + + The user name is then prepended to it, forming + + tim b913a602c7eda7a495b4e6e7334d3890 + + Which is then base64 encoded to meet the requirements of the IMAP4 + AUTHENTICATE command (or the similar POP3 AUTH command), yielding + + dGltIGI5MTNhNjAyYzdlZGE3YTQ5NWI0ZTZlNzMzNGQzODkw + + + + + + + + +Klensin, Catoe & Krumviede Standards Track [Page 3] + +RFC 2195 IMAP/POP AUTHorize Extension September 1997 + + +3. References + + [CHAP] Lloyd, B., and W. Simpson, "PPP Authentication Protocols", + RFC 1334, October 1992. + + [IMAP] Crispin, M., "Internet Message Access Protocol - Version + 4rev1", RFC 2060, University of Washington, December 1996. + + [IMAP-AUTH] Myers, J., "IMAP4 Authentication Mechanisms", + RFC 1731, Carnegie Mellon, December 1994. + + [KEYED-MD5] Krawczyk, Bellare, Canetti, "HMAC: Keyed-Hashing for + Message Authentication", RFC 2104, February 1997. + + [MD5] Rivest, R., "The MD5 Message Digest Algorithm", + RFC 1321, MIT Laboratory for Computer Science, April 1992. + + [POP3] Myers, J., and M. Rose, "Post Office Protocol - Version 3", + STD 53, RFC 1939, Carnegie Mellon, May 1996. + + [POP3-AUTH] Myers, J., "POP3 AUTHentication command", RFC 1734, + Carnegie Mellon, December, 1994. + +4. Security Considerations + + It is conjectured that use of the CRAM authentication mechanism + provides origin identification and replay protection for a session. + Accordingly, a server that implements both a cleartext password + command and this authentication type should not allow both methods of + access for a given user. + + While the saving, on the server, of "contexts" (see section 2) is + marginally better than saving the shared secrets in cleartext as is + required by CHAP [CHAP] and APOP [POP3], it is not sufficient to + protect the secrets if the server itself is compromised. + Consequently, servers that store the secrets or contexts must both be + protected to a level appropriate to the potential information value + in user mailboxes and identities. + + As the length of the shared secret increases, so does the difficulty + of deriving it. + + While there are now suggestions in the literature that the use of MD5 + and keyed MD5 in authentication procedures probably has a limited + effective lifetime, the technique is now widely deployed and widely + understood. It is believed that this general understanding may + assist with the rapid replacement, by CRAM-MD5, of the current uses + of permanent cleartext passwords in IMAP. This document has been + + + +Klensin, Catoe & Krumviede Standards Track [Page 4] + +RFC 2195 IMAP/POP AUTHorize Extension September 1997 + + + deliberately written to permit easy upgrading to use SHA (or whatever + alternatives emerge) when they are considered to be widely available + and adequately safe. + + Even with the use of CRAM, users are still vulnerable to active + attacks. An example of an increasingly common active attack is 'TCP + Session Hijacking' as described in CERT Advisory CA-95:01 [CERT95]. + + See section 1 above for additional discussion. + +5. Acknowledgements + + This memo borrows ideas and some text liberally from [POP3] and + [RFC-1731] and thanks are due the authors of those documents. Ran + Atkinson made a number of valuable technical and editorial + contributions to the document. + +6. Authors' Addresses + + John C. Klensin + MCI Telecommunications + 800 Boylston St, 7th floor + Boston, MA 02199 + USA + + EMail: klensin@mci.net + Phone: +1 617 960 1011 + + Randy Catoe + MCI Telecommunications + 2100 Reston Parkway + Reston, VA 22091 + USA + + EMail: randy@mci.net + Phone: +1 703 715 7366 + + Paul Krumviede + MCI Telecommunications + 2100 Reston Parkway + Reston, VA 22091 + USA + + EMail: paul@mci.net + Phone: +1 703 715 7251 + + + + + + +Klensin, Catoe & Krumviede Standards Track [Page 5] + diff --git a/src/common/libManageSieve/doc/SASL LOGIN/draft-murchison-sasl-login-00.txt b/src/common/libManageSieve/doc/SASL LOGIN/draft-murchison-sasl-login-00.txt new file mode 100644 index 00000000..e6ffc297 --- /dev/null +++ b/src/common/libManageSieve/doc/SASL LOGIN/draft-murchison-sasl-login-00.txt @@ -0,0 +1,396 @@ + + + + + + + +Internet Draft K. Murchison +Category: Informational M. Crispin +Expires: March 2, 2004 28 August 2003 + + + The LOGIN SASL Mechanism + + <draft-murchison-sasl-login-00.txt> + + +Status of this Memo + + This document is an Internet-Draft and is subject to all provisions + of Section 10 of RFC2026. + + Internet-Drafts are working documents of the Internet Engineering + Task Force (IETF), its areas, and its working groups. Note that + other groups may also distribute working documents as + Internet-Drafts. + + Internet-Drafts are draft documents valid for a maximum of six months + and may be updated, replaced, or obsoleted by other documents at any + time. It is inappropriate to use Internet-Drafts as reference + material or to cite them other than as "work in progress." + + The list of current Internet-Drafts can be accessed at + http://www.ietf.org/1id-abstracts.html + + The list of Internet-Draft Shadow Directories can be accessed at + http://www.ietf.org/shadow.html + + +Copyright Notice + + Copyright (C) The Internet Society 2003. All Rights Reserved. + + +Abstract + + This document documents the obsolete clear-text user/password Simple + Authentication and Security Layer (SASL) mechanism called the LOGIN + mechanism. The LOGIN mechanism was intended to be used, in + combination with data confidentiality services provided by a lower + layer, in protocols which lack a simple password authentication + command. + + + + + + +Expires: March 2, 2004 Murchison [Page 1] + +Internet Draft LOGIN SASL Mechanism August 28, 2004 + + + +Conventions Used in the Document + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this + document are to be interpreted as described in [KEYWORDS]. + + +1. Background and Intended Usage + + This document documents the obsolete LOGIN Simple Authentication and + Security Layer ([SASL]) mechanism which was in use in protocols with + no clear-text login command (e.g., [SMTP-AUTH]). + + Note: The LOGIN SASL mechanism is obsoleted in favor of the PLAIN + SASL mechanism ([PLAIN]). The LOGIN mechanism is documented here + only for the purpose of backwards compatibility with legacy software. + Clients SHOULD implement the PLAIN SASL mechanism and use it whenever + offered by a server. The LOGIN SASL mechanism SHOULD NOT be used by + a client when other plaintext mechanisms are offered by a server. + + The name associated with this mechanism is "LOGIN". + + The LOGIN SASL mechanism does not provide a security layer. This + mechanism MUST NOT be used without adequate security protection as + the mechanism affords no integrity nor confidentiality protection + itself. The LOGIN SASL mechanism MUST NOT be advertised or used in + any configuration that prohibits the PLAIN mechanism or plaintext + LOGIN (or USER/PASS) command that sends passwords in the clear. + + +2. LOGIN SASL Mechanism + + The authorization identity is the same string as the "username" in + the traditional (non-SASL) LOGIN or USER commands; the authorization + authenticator is the same string as the traditional "password". The + authentication identity is the same as the authorization identity in + this mechanism. + + Only US-ASCII printable characters SHOULD be used in the username and + password to permit maximal interoperability. If non-US-ASCII + characters are used in a username, they MUST use UTF-8. Passwords + MAY contain arbitrary binary data excluding NUL, CR and LF + characters. However, if a password is supplied to the client as a + sequence of characters (e.g., a password dialog box), those + characters MUST be encoded as UTF-8. + + The username MUST be less than 64 characters in length. + + + +Expires: March 2, 2004 Murchison [Page 2] + +Internet Draft LOGIN SASL Mechanism August 28, 2004 + + +2.1. Client side of authentication protocol exchange + + The client expects the server to issue a challenge. The client then + responds with the authorization identity. The client then expects + the server to issue a second challenge. The client then responds + with the authorization authenticator. The contents of both + challenges SHOULD be ignored. + + +2.2. Server side of authentication protocol exchange + + The server issues the string "User Name" in challenge, and receives a + client response. This response is recorded as the authorization + identity. The server then issues the string "Password" in challenge, + and receives a client response. This response is recorded as the + authorization authenticator. The server must verify that the + authorization authenticator permits login as the authorization + identity. + + Note: There is at least one widely deployed client which requires + that the challenge strings transmitted by the server be "Username:" + and "Password:" respectively. For this reason, server + implementations MAY send these challenge strings instead of those + listed above. + + +2.3. Example + + This example shows the use of the LOGIN mechanism with the SMTP AUTH + command [SMTP-AUTH] under the protection of SMTP STARTTLS [SMTP-TLS]. + The user name is "tim" and the password is "tanstaaftanstaaf". The + base64 encoding of the challenges and responses is part of the SMTP + AUTH command, not part of the LOGIN specification itself. "C:" and + "S:" indicate lines sent by the client and server respectively. + + S: 220 smtp.example.com ESMTP server ready + C: EHLO test.example.com + S: 250-smtp.example.com + S: 250-STARTTLS + S: 250 AUTH CRAM-MD5 + C: STARTTLS + S: 220 Ready to start TLS + <TLS negotiation, further commands are under TLS layer> + C: EHLO test.example.com + S: 250-smtp.example.com + S: 250 AUTH LOGIN CRAM-MD5 + C: AUTH LOGIN + S: 334 VXNlciBOYW1lAA== + + + +Expires: March 2, 2004 Murchison [Page 3] + +Internet Draft LOGIN SASL Mechanism August 28, 2004 + + + C: dGlt + S: 334 UGFzc3dvcmQA + C: dGFuc3RhYWZ0YW5zdGFhZg== + S: 235 Authentication successful. + + +3. + Security Considerations + + The LOGIN mechanism relies upon an underlying encryption layer or + other secure channel for security. When used without an encryption + layer or secure channel, it is vulnerable to a common network + eavesdropping attack. Therefore the LOGIN mechanism MUST NOT be + advertised or used in any configuration that prohibits the PLAIN + mechanism or a plaintext LOGIN (or USER/PASS) command that sends + passwords in the clear. + + +4. + IANA Considerations + + The registration for the LOGIN SASL mechanism follows: + + SASL mechanism name: LOGIN + Security Considerations: See section 3 of this memo + Published specification: this memo + Person & email address to contact for futher information: + See section 7 of this memo + Intended usage: OBSOLETE + Owner/Change controller: See section 7 of this memo + + +5. + References + + +5.1. + Normative References + + + [KEYWORDS] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", Harvard University, RFC 2119, March 1997. + + + [SASL] Melnikov, A., Ed., "Simple Authentication and Security Layer + (SASL)", Isode, draft-ietf-sasl-rfc2222bis-xx.txt, Work In + Progress. + + + + +Expires: March 2, 2004 Murchison [Page 4] + +Internet Draft LOGIN SASL Mechanism August 28, 2004 + + +5.2. Informative References + + + [PLAIN] Zeilenga, Kurt D., Ed., "The Plain SASL Mechanism", + OpenLDAP Foundation, draft-ietf-sasl-plain-xx.txt, Work In + Progress. + + + [SMTP-AUTH] Myers, J., "SMTP Service Extension for Authentication", + Netscape Communications, RFC 2554, March 1999. + + + [SMTP-TLS] Hoffman, P., "SMTP Service Extension for Secure SMTP + over Transport Layer Security", Internet Mail Consortium, RFC + 3207, February 2002. + + + +6. Acknowledgments + + Thanks to Rob Siemborski for his input and feedback on this document. + + +7. + Author's Address + + Kenneth Murchison + Oceana Matrix Ltd. + 21 Princeton Place + Orchard Park, NY 14127 + + Phone: (716) 662-8973 + + EMail: ken@oceana.com + + + + + Mark R. Crispin + Networks and Distributed Computing + University of Washington + 4545 15th Avenue NE + Seattle, WA 98105-4527 + + Phone: (206) 543-5762 + + EMail: MRC@CAC.Washington.EDU + + + + +Expires: March 2, 2004 Murchison [Page 5] + +Internet Draft LOGIN SASL Mechanism August 28, 2004 + + +8. + Intellectual Property Considerations + + The IETF takes no position regarding the validity or scope of any + intellectual property or other rights that might be claimed to + pertain to the implementation or use of the technology described in + this document or the extent to which any license under such rights + might or might not be available; neither does it represent that it has + made any effort to identify any such rights. Information on the + IETF's procedures with respect to rights in standards-track and + standards-related documentation can be found in BCP-11. Copies of + claims of rights made available for publication and any assurances of + licenses to be made available, or the result of an attempt made to + obtain a general license or permission for the use of such proprietary + rights by implementors or users of this specification can be obtained + from the IETF Secretariat. + + The IETF invites any interested party to bring to its attention any + copyrights, patents or patent applications, or other proprietary + rights which may cover technology that may be required to practice + this standard. Please address the information to the IETF Executive + Director. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Expires: March 2, 2004 Murchison [Page 6] + +Internet Draft LOGIN SASL Mechanism August 28, 2004 + + +9. + Full Copyright Statement + + Copyright (C) The Internet Society 2003. All Rights Reserved. + + This document and translations of it may be copied and furnished to + others, and derivative works that comment on or otherwise explain it + or assist in its implmentation may be prepared, copied, published and + distributed, in whole or in part, without restriction of any kind, + provided that the above copyright notice and this paragraph are + included on all such copies and derivative works. However, this + document itself may not be modified in any way, such as by removing + the copyright notice or references to the Internet Society or other + Internet organizations, except as needed for the purpose of + developing Internet standards in which case the procedures for + copyrights defined in the Internet Standards process must be followed, + or as required to translate it into languages other than English. + + The limited permissions granted above are perpetual and will not be + revoked by the Internet Society or its successors or assigns. + + This document and the information contained herein is provided on an + "AS IS" basis and THE INTERNET SOCIETY AND THE INTERNET + ENGINEERING TASK FORCE DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, + INCLUDING BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE + INFORMATION HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED + WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + + + + + + + + + + + + + + + + + + + + + + + + +Expires: March 2, 2004 Murchison [Page 7] + diff --git a/src/common/libManageSieve/doc/SASL PLAIN/rfc4616.txt b/src/common/libManageSieve/doc/SASL PLAIN/rfc4616.txt new file mode 100644 index 00000000..991189d5 --- /dev/null +++ b/src/common/libManageSieve/doc/SASL PLAIN/rfc4616.txt @@ -0,0 +1,619 @@ + + + + + + +Network Working Group K. Zeilenga, Ed. +Request for Comments: 4616 OpenLDAP Foundation +Updates: 2595 August 2006 +Category: Standards Track + + + The PLAIN Simple Authentication and Security Layer (SASL) Mechanism + +Status of This Memo + + This document specifies an Internet standards track protocol for the + Internet community, and requests discussion and suggestions for + improvements. Please refer to the current edition of the "Internet + Official Protocol Standards" (STD 1) for the standardization state + and status of this protocol. Distribution of this memo is unlimited. + +Copyright Notice + + Copyright (C) The Internet Society (2006). + +Abstract + + This document defines a simple clear-text user/password Simple + Authentication and Security Layer (SASL) mechanism called the PLAIN + mechanism. The PLAIN mechanism is intended to be used, in + combination with data confidentiality services provided by a lower + layer, in protocols that lack a simple password authentication + command. + + + + + + + + + + + + + + + + + + + + + + + +Zeilenga Standards Track [Page 1] + +RFC 4616 The PLAIN SASL Mechanism August 2006 + + +1. Introduction + + Clear-text, multiple-use passwords are simple, interoperate with + almost all existing operating system authentication databases, and + are useful for a smooth transition to a more secure password-based + authentication mechanism. The drawback is that they are unacceptable + for use over network connections where data confidentiality is not + ensured. + + This document defines the PLAIN Simple Authentication and Security + Layer ([SASL]) mechanism for use in protocols with no clear-text + login command (e.g., [ACAP] or [SMTP-AUTH]). This document updates + RFC 2595, replacing Section 6. Changes since RFC 2595 are detailed + in Appendix A. + + The name associated with this mechanism is "PLAIN". + + The PLAIN SASL mechanism does not provide a security layer. + + The PLAIN mechanism should not be used without adequate data security + protection as this mechanism affords no integrity or confidentiality + protections itself. The mechanism is intended to be used with data + security protections provided by application-layer protocol, + generally through its use of Transport Layer Security ([TLS]) + services. + + By default, implementations SHOULD advertise and make use of the + PLAIN mechanism only when adequate data security services are in + place. Specifications for IETF protocols that indicate that this + mechanism is an applicable authentication mechanism MUST mandate that + implementations support an strong data security service, such as TLS. + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this + document are to be interpreted as described in [Keywords]. + +2. PLAIN SASL Mechanism + + The mechanism consists of a single message, a string of [UTF-8] + encoded [Unicode] characters, from the client to the server. The + client presents the authorization identity (identity to act as), + followed by a NUL (U+0000) character, followed by the authentication + identity (identity whose password will be used), followed by a NUL + (U+0000) character, followed by the clear-text password. As with + other SASL mechanisms, the client does not provide an authorization + identity when it wishes the server to derive an identity from the + credentials and use that as the authorization identity. + + + + +Zeilenga Standards Track [Page 2] + +RFC 4616 The PLAIN SASL Mechanism August 2006 + + + The formal grammar for the client message using Augmented BNF [ABNF] + follows. + + message = [authzid] UTF8NUL authcid UTF8NUL passwd + authcid = 1*SAFE ; MUST accept up to 255 octets + authzid = 1*SAFE ; MUST accept up to 255 octets + passwd = 1*SAFE ; MUST accept up to 255 octets + UTF8NUL = %x00 ; UTF-8 encoded NUL character + + SAFE = UTF1 / UTF2 / UTF3 / UTF4 + ;; any UTF-8 encoded Unicode character except NUL + + UTF1 = %x01-7F ;; except NUL + UTF2 = %xC2-DF UTF0 + UTF3 = %xE0 %xA0-BF UTF0 / %xE1-EC 2(UTF0) / + %xED %x80-9F UTF0 / %xEE-EF 2(UTF0) + UTF4 = %xF0 %x90-BF 2(UTF0) / %xF1-F3 3(UTF0) / + %xF4 %x80-8F 2(UTF0) + UTF0 = %x80-BF + + The authorization identity (authzid), authentication identity + (authcid), password (passwd), and NUL character deliminators SHALL be + transferred as [UTF-8] encoded strings of [Unicode] characters. As + the NUL (U+0000) character is used as a deliminator, the NUL (U+0000) + character MUST NOT appear in authzid, authcid, or passwd productions. + + The form of the authzid production is specific to the application- + level protocol's SASL profile [SASL]. The authcid and passwd + productions are form-free. Use of non-visible characters or + characters that a user may be unable to enter on some keyboards is + discouraged. + + Servers MUST be capable of accepting authzid, authcid, and passwd + productions up to and including 255 octets. It is noted that the + UTF-8 encoding of a Unicode character may be as long as 4 octets. + + Upon receipt of the message, the server will verify the presented (in + the message) authentication identity (authcid) and password (passwd) + with the system authentication database, and it will verify that the + authentication credentials permit the client to act as the (presented + or derived) authorization identity (authzid). If both steps succeed, + the user is authenticated. + + The presented authentication identity and password strings, as well + as the database authentication identity and password strings, are to + be prepared before being used in the verification process. The + [SASLPrep] profile of the [StringPrep] algorithm is the RECOMMENDED + preparation algorithm. The SASLprep preparation algorithm is + + + +Zeilenga Standards Track [Page 3] + +RFC 4616 The PLAIN SASL Mechanism August 2006 + + + recommended to improve the likelihood that comparisons behave in an + expected manner. The SASLprep preparation algorithm is not mandatory + so as to allow the server to employ other preparation algorithms + (including none) when appropriate. For instance, use of a different + preparation algorithm may be necessary for the server to interoperate + with an external system. + + When preparing the presented strings using [SASLPrep], the presented + strings are to be treated as "query" strings (Section 7 of + [StringPrep]) and hence unassigned code points are allowed to appear + in their prepared output. When preparing the database strings using + [SASLPrep], the database strings are to be treated as "stored" + strings (Section 7 of [StringPrep]) and hence unassigned code points + are prohibited from appearing in their prepared output. + + Regardless of the preparation algorithm used, if the output of a + non-invertible function (e.g., hash) of the expected string is + stored, the string MUST be prepared before input to that function. + + Regardless of the preparation algorithm used, if preparation fails or + results in an empty string, verification SHALL fail. + + When no authorization identity is provided, the server derives an + authorization identity from the prepared representation of the + provided authentication identity string. This ensures that the + derivation of different representations of the authentication + identity produces the same authorization identity. + + The server MAY use the credentials to initialize any new + authentication database, such as one suitable for [CRAM-MD5] or + [DIGEST-MD5]. + +3. Pseudo-Code + + This section provides pseudo-code illustrating the verification + process (using hashed passwords and the SASLprep preparation + function) discussed above. This section is not definitive. + + boolean Verify(string authzid, string authcid, string passwd) { + string pAuthcid = SASLprep(authcid, true); # prepare authcid + string pPasswd = SASLprep(passwd, true); # prepare passwd + if (pAuthcid == NULL || pPasswd == NULL) { + return false; # preparation failed + } + if (pAuthcid == "" || pPasswd == "") { + return false; # empty prepared string + } + + + + +Zeilenga Standards Track [Page 4] + +RFC 4616 The PLAIN SASL Mechanism August 2006 + + + storedHash = FetchPasswordHash(pAuthcid); + if (storedHash == NULL || storedHash == "") { + return false; # error or unknown authcid + } + + if (!Compare(storedHash, Hash(pPasswd))) { + return false; # incorrect password + } + + if (authzid == NULL ) { + authzid = DeriveAuthzid(pAuthcid); + if (authzid == NULL || authzid == "") { + return false; # could not derive authzid + } + } + + if (!Authorize(pAuthcid, authzid)) { + return false; # not authorized + } + + return true; + } + + The second parameter of the SASLprep function, when true, indicates + that unassigned code points are allowed in the input. When the + SASLprep function is called to prepare the password prior to + computing the stored hash, the second parameter would be false. + + The second parameter provided to the Authorize function is not + prepared by this code. The application-level SASL profile should be + consulted to determine what, if any, preparation is necessary. + + Note that the DeriveAuthzid and Authorize functions (whether + implemented as one function or two, whether designed in a manner in + which these functions or whether the mechanism implementation can be + reused elsewhere) require knowledge and understanding of mechanism + and the application-level protocol specification and/or + implementation details to implement. + + Note that the Authorize function outcome is clearly dependent on + details of the local authorization model and policy. Both functions + may be dependent on other factors as well. + + + + + + + + + +Zeilenga Standards Track [Page 5] + +RFC 4616 The PLAIN SASL Mechanism August 2006 + + +4. Examples + + This section provides examples of PLAIN authentication exchanges. + The examples are intended to help the readers understand the above + text. The examples are not definitive. + + "C:" and "S:" indicate lines sent by the client and server, + respectively. "<NUL>" represents a single NUL (U+0000) character. + The Application Configuration Access Protocol ([ACAP]) is used in the + examples. + + The first example shows how the PLAIN mechanism might be used for + user authentication. + + S: * ACAP (SASL "CRAM-MD5") (STARTTLS) + C: a001 STARTTLS + S: a001 OK "Begin TLS negotiation now" + <TLS negotiation, further commands are under TLS layer> + S: * ACAP (SASL "CRAM-MD5" "PLAIN") + C: a002 AUTHENTICATE "PLAIN" + S: + "" + C: {21} + C: <NUL>tim<NUL>tanstaaftanstaaf + S: a002 OK "Authenticated" + + The second example shows how the PLAIN mechanism might be used to + attempt to assume the identity of another user. In this example, the + server rejects the request. Also, this example makes use of the + protocol optional initial response capability to eliminate a round- + trip. + + S: * ACAP (SASL "CRAM-MD5") (STARTTLS) + C: a001 STARTTLS + S: a001 OK "Begin TLS negotiation now" + <TLS negotiation, further commands are under TLS layer> + S: * ACAP (SASL "CRAM-MD5" "PLAIN") + C: a002 AUTHENTICATE "PLAIN" {20+} + C: Ursel<NUL>Kurt<NUL>xipj3plmq + S: a002 NO "Not authorized to requested authorization identity" + +5. Security Considerations + + As the PLAIN mechanism itself provided no integrity or + confidentiality protections, it should not be used without adequate + external data security protection, such as TLS services provided by + many application-layer protocols. By default, implementations SHOULD + NOT advertise and SHOULD NOT make use of the PLAIN mechanism unless + adequate data security services are in place. + + + +Zeilenga Standards Track [Page 6] + +RFC 4616 The PLAIN SASL Mechanism August 2006 + + + When the PLAIN mechanism is used, the server gains the ability to + impersonate the user to all services with the same password + regardless of any encryption provided by TLS or other confidentiality + protection mechanisms. Whereas many other authentication mechanisms + have similar weaknesses, stronger SASL mechanisms address this issue. + Clients are encouraged to have an operational mode where all + mechanisms that are likely to reveal the user's password to the + server are disabled. + + General [SASL] security considerations apply to this mechanism. + + Unicode, [UTF-8], and [StringPrep] security considerations also + apply. + +6. IANA Considerations + + The SASL Mechanism registry [IANA-SASL] entry for the PLAIN mechanism + has been updated by the IANA to reflect that this document now + provides its technical specification. + + To: iana@iana.org + Subject: Updated Registration of SASL mechanism PLAIN + + SASL mechanism name: PLAIN + Security considerations: See RFC 4616. + Published specification (optional, recommended): RFC 4616 + Person & email address to contact for further information: + Kurt Zeilenga <kurt@openldap.org> + IETF SASL WG <ietf-sasl@imc.org> + Intended usage: COMMON + Author/Change controller: IESG <iesg@ietf.org> + Note: Updates existing entry for PLAIN + +7. Acknowledgements + + This document is a revision of RFC 2595 by Chris Newman. Portions of + the grammar defined in Section 2 were borrowed from [UTF-8] by + Francois Yergeau. + + This document is a product of the IETF Simple Authentication and + Security Layer (SASL) Working Group. + + + + + + + + + + +Zeilenga Standards Track [Page 7] + +RFC 4616 The PLAIN SASL Mechanism August 2006 + + +8. Normative References + + [ABNF] Crocker, D., Ed. and P. Overell, "Augmented BNF for + Syntax Specifications: ABNF", RFC 4234, October 2005. + + [Keywords] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, March 1997. + + [SASL] Melnikov, A., Ed., and K. Zeilenga, Ed., "Simple + Authentication and Security Layer (SASL)", RFC 4422, + June 2006. + + [SASLPrep] Zeilenga, K., "SASLprep: Stringprep Profile for User + Names and Passwords", RFC 4013, February 2005. + + [StringPrep] Hoffman, P. and M. Blanchet, "Preparation of + Internationalized Strings ("stringprep")", RFC 3454, + December 2002. + + [Unicode] The Unicode Consortium, "The Unicode Standard, Version + 3.2.0" is defined by "The Unicode Standard, Version + 3.0" (Reading, MA, Addison-Wesley, 2000. ISBN 0-201- + 61633-5), as amended by the "Unicode Standard Annex + #27: Unicode 3.1" + (http://www.unicode.org/reports/tr27/) and by the + "Unicode Standard Annex #28: Unicode 3.2" + (http://www.unicode.org/reports/tr28/). + + [UTF-8] Yergeau, F., "UTF-8, a transformation format of ISO + 10646", STD 63, RFC 3629, November 2003. + + [TLS] Dierks, T. and E. Rescorla, "The Transport Layer + Security (TLS) Protocol Version 1.1", RFC 4346, April + 2006. + +9. Informative References + + [ACAP] Newman, C. and J. Myers, "ACAP -- Application + Configuration Access Protocol", RFC 2244, November + 1997. + + [CRAM-MD5] Nerenberg, L., Ed., "The CRAM-MD5 SASL Mechanism", Work + in Progress, June 2006. + + [DIGEST-MD5] Melnikov, A., Ed., "Using Digest Authentication as a + SASL Mechanism", Work in Progress, June 2006. + + + + + +Zeilenga Standards Track [Page 8] + +RFC 4616 The PLAIN SASL Mechanism August 2006 + + + [IANA-SASL] IANA, "SIMPLE AUTHENTICATION AND SECURITY LAYER (SASL) + MECHANISMS", + <http://www.iana.org/assignments/sasl-mechanisms>. + + [SMTP-AUTH] Myers, J., "SMTP Service Extension for Authentication", + RFC 2554, March 1999. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Zeilenga Standards Track [Page 9] + +RFC 4616 The PLAIN SASL Mechanism August 2006 + + +Appendix A. Changes since RFC 2595 + + This appendix is non-normative. + + This document replaces Section 6 of RFC 2595. + + The specification details how the server is to compare client- + provided character strings with stored character strings. + + The ABNF grammar was updated. In particular, the grammar now allows + LINE FEED (U+000A) and CARRIAGE RETURN (U+000D) characters in the + authzid, authcid, passwd productions. However, whether these control + characters may be used depends on the string preparation rules + applicable to the production. For passwd and authcid productions, + control characters are prohibited. For authzid, one must consult the + application-level SASL profile. This change allows PLAIN to carry + all possible authorization identity strings allowed in SASL. + + Pseudo-code was added. + + The example section was expanded to illustrate more features of the + PLAIN mechanism. + +Editor's Address + + Kurt D. Zeilenga + OpenLDAP Foundation + + EMail: Kurt@OpenLDAP.org + + + + + + + + + + + + + + + + + + + + + + +Zeilenga Standards Track [Page 10] + +RFC 4616 The PLAIN SASL Mechanism August 2006 + + +Full Copyright Statement + + Copyright (C) The Internet Society (2006). + + This document is subject to the rights, licenses and restrictions + contained in BCP 78, and except as set forth therein, the authors + retain all their rights. + + This document and the information contained herein are provided on an + "AS IS" basis and THE CONTRIBUTOR, THE ORGANIZATION HE/SHE REPRESENTS + OR IS SPONSORED BY (IF ANY), THE INTERNET SOCIETY AND THE INTERNET + ENGINEERING TASK FORCE DISCLAIM ALL WARRANTIES, EXPRESS OR IMPLIED, + INCLUDING BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE + INFORMATION HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED + WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + +Intellectual Property + + The IETF takes no position regarding the validity or scope of any + Intellectual Property Rights or other rights that might be claimed to + pertain to the implementation or use of the technology described in + this document or the extent to which any license under such rights + might or might not be available; nor does it represent that it has + made any independent effort to identify any such rights. Information + on the procedures with respect to rights in RFC documents can be + found in BCP 78 and BCP 79. + + Copies of IPR disclosures made to the IETF Secretariat and any + assurances of licenses to be made available, or the result of an + attempt made to obtain a general license or permission for the use of + such proprietary rights by implementers or users of this + specification can be obtained from the IETF on-line IPR repository at + http://www.ietf.org/ipr. + + The IETF invites any interested party to bring to its attention any + copyrights, patents or patent applications, or other proprietary + rights that may cover technology that may be required to implement + this standard. Please address the information to the IETF at + ietf-ipr@ietf.org. + +Acknowledgement + + Funding for the RFC Editor function is provided by the IETF + Administrative Support Activity (IASA). + + + + + + + +Zeilenga Standards Track [Page 11] + diff --git a/src/common/libManageSieve/doc/SASL SCRAM/PKCS #5/rfc2898.txt b/src/common/libManageSieve/doc/SASL SCRAM/PKCS #5/rfc2898.txt new file mode 100644 index 00000000..79af9b84 --- /dev/null +++ b/src/common/libManageSieve/doc/SASL SCRAM/PKCS #5/rfc2898.txt @@ -0,0 +1,1907 @@ + + + + + + +Network Working Group B. Kaliski +Request for Comments: 2898 RSA Laboratories +Category: Informational September 2000 + + + PKCS #5: Password-Based Cryptography Specification + Version 2.0 + +Status of this Memo + + This memo provides information for the Internet community. It does + not specify an Internet standard of any kind. Distribution of this + memo is unlimited. + +Copyright Notice + + Copyright (C) The Internet Society (2000). All Rights Reserved. + +Abstract + + This memo represents a republication of PKCS #5 v2.0 from RSA + Laboratories' Public-Key Cryptography Standards (PKCS) series, and + change control is retained within the PKCS process. The body of this + document, except for the security considerations section, is taken + directly from that specification. + + This document provides recommendations for the implementation of + password-based cryptography, covering key derivation functions, + encryption schemes, message-authentication schemes, and ASN.1 syntax + identifying the techniques. + + The recommendations are intended for general application within + computer and communications systems, and as such include a fair + amount of flexibility. They are particularly intended for the + protection of sensitive information such as private keys, as in PKCS + #8 [25]. It is expected that application standards and implementation + profiles based on these specifications may include additional + constraints. + + Other cryptographic techniques based on passwords, such as password- + based key entity authentication and key establishment protocols + [4][5][26] are outside the scope of this document. Guidelines for + the selection of passwords are also outside the scope. + + + + + + + + +Kaliski Informational [Page 1] + +RFC 2898 Password-Based Cryptography September 2000 + + +Table of Contents + + 1. Introduction ............................................... 3 + 2. Notation ................................................... 3 + 3. Overview ................................................... 4 + 4. Salt and iteration count ................................... 6 + 4.1 Salt ................................................... 6 + 4.2 Iteration count ........................................ 8 + 5. Key derivation functions ................................... 8 + 5.1 PBKDF1 ................................................. 9 + 5.2 PBKDF2 ................................................. 9 + 6. Encryption schemes ......................................... 11 + 6.1 PBES1 .................................................. 12 + 6.1.1 Encryption operation ............................ 12 + 6.1.2 Decryption operation ............................ 13 + 6.2 PBES2 .................................................. 14 + 6.2.1 Encryption operation ............................ 14 + 6.2.2 Decryption operation ............................ 15 + 7. Message authentication schemes ............................. 15 + 7.1 PBMAC1 ................................................. 16 + 7.1.1 MAC generation .................................. 16 + 7.1.2 MAC verification ................................ 16 + 8. Security Considerations .................................... 17 + 9. Author's Address............................................ 17 + A. ASN.1 syntax ............................................... 18 + A.1 PBKDF1 ................................................. 18 + A.2 PBKDF2 ................................................. 18 + A.3 PBES1 .................................................. 20 + A.4 PBES2 .................................................. 20 + A.5 PBMAC1 ................................................. 21 + B. Supporting techniques ...................................... 22 + B.1 Pseudorandom functions ................................. 22 + B.2 Encryption schemes ..................................... 23 + B.3 Message authentication schemes ......................... 26 + C. ASN.1 module ............................................... 26 + Intellectual Property Considerations ............................ 30 + Revision history ................................................ 30 + References ...................................................... 31 + Contact Information & About PKCS ................................ 33 + Full Copyright Statement ........................................ 34 + + + + + + + + + + + +Kaliski Informational [Page 2] + +RFC 2898 Password-Based Cryptography September 2000 + + +1. Introduction + + This document provides recommendations for the implementation of + password-based cryptography, covering the following aspects: + + - key derivation functions + - encryption schemes + - message-authentication schemes + - ASN.1 syntax identifying the techniques + + The recommendations are intended for general application within + computer and communications systems, and as such include a fair + amount of flexibility. They are particularly intended for the + protection of sensitive information such as private keys, as in PKCS + #8 [25]. It is expected that application standards and implementation + profiles based on these specifications may include additional + constraints. + + Other cryptographic techniques based on passwords, such as password- + based key entity authentication and key establishment protocols + [4][5][26] are outside the scope of this document. Guidelines for + the selection of passwords are also outside the scope. + + This document supersedes PKCS #5 version 1.5 [24], but includes + compatible techniques. + +2. Notation + + C ciphertext, an octet string + + c iteration count, a positive integer + + DK derived key, an octet string + + dkLen length in octets of derived key, a positive integer + + EM encoded message, an octet string + + Hash underlying hash function + + hLen length in octets of pseudorandom function output, a positive + integer + + l length in blocks of derived key, a positive integer + + IV initialization vector, an octet string + + K encryption key, an octet string + + + +Kaliski Informational [Page 3] + +RFC 2898 Password-Based Cryptography September 2000 + + + KDF key derivation function + + M message, an octet string + + P password, an octet string + + PRF underlying pseudorandom function + + PS padding string, an octet string + + psLen length in octets of padding string, a positive integer + + S salt, an octet string + + T message authentication code, an octet string + + T_1, ..., T_l, U_1, ..., U_c + intermediate values, octet strings + + 01, 02, ..., 08 + octets with value 1, 2, ..., 8 + + \xor bit-wise exclusive-or of two octet strings + + || || octet length operator + + || concatenation operator + + <i..j> substring extraction operator: extracts octets i through j, + 0 <= i <= j + +3. Overview + + In many applications of public-key cryptography, user security is + ultimately dependent on one or more secret text values or passwords. + Since a password is not directly applicable as a key to any + conventional cryptosystem, however, some processing of the password + is required to perform cryptographic operations with it. Moreover, as + passwords are often chosen from a relatively small space, special + care is required in that processing to defend against search attacks. + + A general approach to password-based cryptography, as described by + Morris and Thompson [8] for the protection of password tables, is to + combine a password with a salt to produce a key. The salt can be + viewed as an index into a large set of keys derived from the + password, and need not be kept secret. Although it may be possible + for an opponent to construct a table of possible passwords (a so- + called "dictionary attack"), constructing a table of possible keys + + + +Kaliski Informational [Page 4] + +RFC 2898 Password-Based Cryptography September 2000 + + + will be difficult, since there will be many possible keys for each + password. An opponent will thus be limited to searching through + passwords separately for each salt. + + Another approach to password-based cryptography is to construct key + derivation techniques that are relatively expensive, thereby + increasing the cost of exhaustive search. One way to do this is to + include an iteration count in the key derivation technique, + indicating how many times to iterate some underlying function by + which keys are derived. A modest number of iterations, say 1000, is + not likely to be a burden for legitimate parties when computing a + key, but will be a significant burden for opponents. + + Salt and iteration count formed the basis for password-based + encryption in PKCS #5 v1.5, and adopted here as well for the various + cryptographic operations. Thus, password-based key derivation as + defined here is a function of a password, a salt, and an iteration + count, where the latter two quantities need not be kept secret. + + From a password-based key derivation function, it is straightforward + to define password-based encryption and message authentication + schemes. As in PKCS #5 v1.5, the password-based encryption schemes + here are based on an underlying, conventional encryption scheme, + where the key for the conventional scheme is derived from the + password. Similarly, the password-based message authentication scheme + is based on an underlying conventional scheme. This two-layered + approach makes the password-based techniques modular in terms of the + underlying techniques they can be based on. + + It is expected that the password-based key derivation functions may + find other applications than just the encryption and message + authentication schemes defined here. For instance, one might derive a + set of keys with a single application of a key derivation function, + rather than derive each key with a separate application of the + function. The keys in the set would be obtained as substrings of the + output of the key derivation function. This approach might be + employed as part of key establishment in a session-oriented protocol. + Another application is password checking, where the output of the key + derivation function is stored (along with the salt and iteration + count) for the purposes of subsequent verification of a password. + + Throughout this document, a password is considered to be an octet + string of arbitrary length whose interpretation as a text string is + unspecified. In the interest of interoperability, however, it is + recommended that applications follow some common text encoding rules. + ASCII and UTF-8 [27] are two possibilities. (ASCII is a subset of + UTF-8.) + + + + +Kaliski Informational [Page 5] + +RFC 2898 Password-Based Cryptography September 2000 + + + Although the selection of passwords is outside the scope of this + document, guidelines have been published [17] that may well be taken + into account. + +4. Salt and Iteration Count + + Inasmuch as salt and iteration count are central to the techniques + defined in this document, some further discussion is warranted. + +4.1 Salt + + A salt in password-based cryptography has traditionally served the + purpose of producing a large set of keys corresponding to a given + password, among which one is selected at random according to the + salt. An individual key in the set is selected by applying a key + derivation function KDF, as + + DK = KDF (P, S) + + where DK is the derived key, P is the password, and S is the salt. + This has two benefits: + + 1. It is difficult for an opponent to precompute all the keys + corresponding to a dictionary of passwords, or even the most + likely keys. If the salt is 64 bits long, for instance, there + will be as many as 2^64 keys for each password. An opponent is + thus limited to searching for passwords after a password-based + operation has been performed and the salt is known. + + 2. It is unlikely that the same key will be selected twice. + Again, if the salt is 64 bits long, the chance of "collision" + between keys does not become significant until about 2^32 keys + have been produced, according to the Birthday Paradox. This + addresses some of the concerns about interactions between + multiple uses of the same key, which may apply for some + encryption and authentication techniques. + + In password-based encryption, the party encrypting a message can gain + assurance that these benefits are realized simply by selecting a + large and sufficiently random salt when deriving an encryption key + from a password. A party generating a message authentication code can + gain such assurance in a similar fashion. + + The party decrypting a message or verifying a message authentication + code, however, cannot be sure that a salt supplied by another party + has actually been generated at random. It is possible, for instance, + that the salt may have been copied from another password-based + operation, in an attempt to exploit interactions between multiple + + + +Kaliski Informational [Page 6] + +RFC 2898 Password-Based Cryptography September 2000 + + + uses of the same key. For instance, suppose two legitimate parties + exchange a encrypted message, where the encryption key is an 80-bit + key derived from a shared password with some salt. An opponent could + take the salt from that encryption and provide it to one of the + parties as though it were for a 40-bit key. If the party reveals the + result of decryption with the 40-bit key, the opponent may be able to + solve for the 40-bit key. In the case that 40-bit key is the first + half of the 80-bit key, the opponent can then readily solve for the + remaining 40 bits of the 80-bit key. + + To defend against such attacks, either the interaction between + multiple uses of the same key should be carefully analyzed, or the + salt should contain data that explicitly distinguishes between + different operations. For instance, the salt might have an + additional, non-random octet that specifies whether the derived key + is for encryption, for message authentication, or for some other + operation. + + Based on this, the following is recommended for salt selection: + + 1. If there is no concern about interactions between multiple uses + of the same key (or a prefix of that key) with the password- + based encryption and authentication techniques supported for a + given password, then the salt may be generated at random and + need not be checked for a particular format by the party + receiving the salt. It should be at least eight octets (64 + bits) long. + + 2. Otherwise, the salt should contain data that explicitly + distinguishes between different operations and different key + lengths, in addition to a random part that is at least eight + octets long, and this data should be checked or regenerated by + the party receiving the salt. For instance, the salt could have + an additional non-random octet that specifies the purpose of + the derived key. Alternatively, it could be the encoding of a + structure that specifies detailed information about the derived + key, such as the encryption or authentication technique and a + sequence number among the different keys derived from the + password. The particular format of the additional data is left + to the application. + + Note. If a random number generator or pseudorandom generator is not + available, a deterministic alternative for generating the salt (or + the random part of it) is to apply a password-based key derivation + function to the password and the message M to be processed. For + instance, the salt could be computed with a key derivation function + as S = KDF (P, M). This approach is not recommended if the message M + + + + +Kaliski Informational [Page 7] + +RFC 2898 Password-Based Cryptography September 2000 + + + is known to belong to a small message space (e.g., "Yes" or "No"), + however, since then there will only be a small number of possible + salts. + +4.2 Iteration Count + + An iteration count has traditionally served the purpose of increasing + the cost of producing keys from a password, thereby also increasing + the difficulty of attack. For the methods in this document, a minimum + of 1000 iterations is recommended. This will increase the cost of + exhaustive search for passwords significantly, without a noticeable + impact in the cost of deriving individual keys. + +5. Key Derivation Functions + + A key derivation function produces a derived key from a base key and + other parameters. In a password-based key derivation function, the + base key is a password and the other parameters are a salt value and + an iteration count, as outlined in Section 3. + + The primary application of the password-based key derivation + functions defined here is in the encryption schemes in Section 6 and + the message authentication scheme in Section 7. Other applications + are certainly possible, hence the independent definition of these + functions. + + Two functions are specified in this section: PBKDF1 and PBKDF2. + PBKDF2 is recommended for new applications; PBKDF1 is included only + for compatibility with existing applications, and is not recommended + for new applications. + + A typical application of the key derivation functions defined here + might include the following steps: + + 1. Select a salt S and an iteration count c, as outlined in + Section 4. + + 2. Select a length in octets for the derived key, dkLen. + + 3. Apply the key derivation function to the password, the salt, + the iteration count and the key length to produce a derived + key. + + 4. Output the derived key. + + Any number of keys may be derived from a password by varying the + salt, as described in Section 3. + + + + +Kaliski Informational [Page 8] + +RFC 2898 Password-Based Cryptography September 2000 + + +5.1 PBKDF1 + + PBKDF1 applies a hash function, which shall be MD2 [6], MD5 [19] or + SHA-1 [18], to derive keys. The length of the derived key is bounded + by the length of the hash function output, which is 16 octets for MD2 + and MD5 and 20 octets for SHA-1. PBKDF1 is compatible with the key + derivation process in PKCS #5 v1.5. + + PBKDF1 is recommended only for compatibility with existing + applications since the keys it produces may not be large enough for + some applications. + + PBKDF1 (P, S, c, dkLen) + + Options: Hash underlying hash function + + Input: P password, an octet string + S salt, an eight-octet string + c iteration count, a positive integer + dkLen intended length in octets of derived key, + a positive integer, at most 16 for MD2 or + MD5 and 20 for SHA-1 + + Output: DK derived key, a dkLen-octet string + + Steps: + + 1. If dkLen > 16 for MD2 and MD5, or dkLen > 20 for SHA-1, output + "derived key too long" and stop. + + 2. Apply the underlying hash function Hash for c iterations to the + concatenation of the password P and the salt S, then extract + the first dkLen octets to produce a derived key DK: + + T_1 = Hash (P || S) , + T_2 = Hash (T_1) , + ... + T_c = Hash (T_{c-1}) , + DK = Tc<0..dkLen-1> + + 3. Output the derived key DK. + +5.2 PBKDF2 + + PBKDF2 applies a pseudorandom function (see Appendix B.1 for an + example) to derive keys. The length of the derived key is essentially + unbounded. (However, the maximum effective search space for the + + + + +Kaliski Informational [Page 9] + +RFC 2898 Password-Based Cryptography September 2000 + + + derived key may be limited by the structure of the underlying + pseudorandom function. See Appendix B.1 for further discussion.) + PBKDF2 is recommended for new applications. + + PBKDF2 (P, S, c, dkLen) + + Options: PRF underlying pseudorandom function (hLen + denotes the length in octets of the + pseudorandom function output) + + Input: P password, an octet string + S salt, an octet string + c iteration count, a positive integer + dkLen intended length in octets of the derived + key, a positive integer, at most + (2^32 - 1) * hLen + + Output: DK derived key, a dkLen-octet string + + Steps: + + 1. If dkLen > (2^32 - 1) * hLen, output "derived key too long" and + stop. + + 2. Let l be the number of hLen-octet blocks in the derived key, + rounding up, and let r be the number of octets in the last + block: + + l = CEIL (dkLen / hLen) , + r = dkLen - (l - 1) * hLen . + + Here, CEIL (x) is the "ceiling" function, i.e. the smallest + integer greater than, or equal to, x. + + 3. For each block of the derived key apply the function F defined + below to the password P, the salt S, the iteration count c, and + the block index to compute the block: + + T_1 = F (P, S, c, 1) , + T_2 = F (P, S, c, 2) , + ... + T_l = F (P, S, c, l) , + + where the function F is defined as the exclusive-or sum of the + first c iterates of the underlying pseudorandom function PRF + applied to the password P and the concatenation of the salt S + and the block index i: + + + + +Kaliski Informational [Page 10] + +RFC 2898 Password-Based Cryptography September 2000 + + + F (P, S, c, i) = U_1 \xor U_2 \xor ... \xor U_c + + where + + U_1 = PRF (P, S || INT (i)) , + U_2 = PRF (P, U_1) , + ... + U_c = PRF (P, U_{c-1}) . + + Here, INT (i) is a four-octet encoding of the integer i, most + significant octet first. + + 4. Concatenate the blocks and extract the first dkLen octets to + produce a derived key DK: + + DK = T_1 || T_2 || ... || T_l<0..r-1> + + 5. Output the derived key DK. + + Note. The construction of the function F follows a "belt-and- + suspenders" approach. The iterates U_i are computed recursively to + remove a degree of parallelism from an opponent; they are exclusive- + ored together to reduce concerns about the recursion degenerating + into a small set of values. + +6. Encryption Schemes + + An encryption scheme, in the symmetric setting, consists of an + encryption operation and a decryption operation, where the encryption + operation produces a ciphertext from a message under a key, and the + decryption operation recovers the message from the ciphertext under + the same key. In a password-based encryption scheme, the key is a + password. + + A typical application of a password-based encryption scheme is a + private-key protection method, where the message contains private-key + information, as in PKCS #8. The encryption schemes defined here would + be suitable encryption algorithms in that context. + + Two schemes are specified in this section: PBES1 and PBES2. PBES2 is + recommended for new applications; PBES1 is included only for + compatibility with existing applications, and is not recommended for + new applications. + + + + + + + + +Kaliski Informational [Page 11] + +RFC 2898 Password-Based Cryptography September 2000 + + +6.1 PBES1 + + PBES1 combines the PBKDF1 function (Section 5.1) with an underlying + block cipher, which shall be either DES [15] or RC2(tm) [21] in CBC + mode [16]. PBES1 is compatible with the encryption scheme in PKCS #5 + v1.5. + + PBES1 is recommended only for compatibility with existing + applications, since it supports only two underlying encryption + schemes, each of which has a key size (56 or 64 bits) that may not be + large enough for some applications. + +6.1.1 Encryption Operation + + The encryption operation for PBES1 consists of the following steps, + which encrypt a message M under a password P to produce a ciphertext + C: + + 1. Select an eight-octet salt S and an iteration count c, as + outlined in Section 4. + + 2. Apply the PBKDF1 key derivation function (Section 5.1) to the + password P, the salt S, and the iteration count c to produce at + derived key DK of length 16 octets: + + DK = PBKDF1 (P, S, c, 16) . + + 3. Separate the derived key DK into an encryption key K consisting + of the first eight octets of DK and an initialization vector IV + consisting of the next eight octets: + + K = DK<0..7> , + IV = DK<8..15> . + + 4. Concatenate M and a padding string PS to form an encoded + message EM: + + EM = M || PS , + + where the padding string PS consists of 8-(||M|| mod 8) octets + each with value 8-(||M|| mod 8). The padding string PS will + satisfy one of the following statements: + + PS = 01, if ||M|| mod 8 = 7 ; + PS = 02 02, if ||M|| mod 8 = 6 ; + ... + PS = 08 08 08 08 08 08 08 08, if ||M|| mod 8 = 0. + + + + +Kaliski Informational [Page 12] + +RFC 2898 Password-Based Cryptography September 2000 + + + The length in octets of the encoded message will be a multiple + of eight and it will be possible to recover the message M + unambiguously from the encoded message. (This padding rule is + taken from RFC 1423 [3].) + + 5. Encrypt the encoded message EM with the underlying block cipher + (DES or RC2) in cipher block chaining mode under the encryption + key K with initialization vector IV to produce the ciphertext + C. For DES, the key K shall be considered as a 64-bit encoding + of a 56-bit DES key with parity bits ignored (see [9]). For + RC2, the "effective key bits" shall be 64 bits. + + 6. Output the ciphertext C. + + The salt S and the iteration count c may be conveyed to the party + performing decryption in an AlgorithmIdentifier value (see Appendix + A.3). + +6.1.2 Decryption Operation + + The decryption operation for PBES1 consists of the following steps, + which decrypt a ciphertext C under a password P to recover a message + M: + + 1. Obtain the eight-octet salt S and the iteration count c. + + 2. Apply the PBKDF1 key derivation function (Section 5.1) to the + password P, the salt S, and the iteration count c to produce a + derived key DK of length 16 octets: + + DK = PBKDF1 (P, S, c, 16) + + 3. Separate the derived key DK into an encryption key K consisting + of the first eight octets of DK and an initialization vector IV + consisting of the next eight octets: + + K = DK<0..7> , + IV = DK<8..15> . + + 4. Decrypt the ciphertext C with the underlying block cipher (DES + or RC2) in cipher block chaining mode under the encryption key + K with initialization vector IV to recover an encoded message + EM. If the length in octets of the ciphertext C is not a + multiple of eight, output "decryption error" and stop. + + 5. Separate the encoded message EM into a message M and a padding + string PS: + + + + +Kaliski Informational [Page 13] + +RFC 2898 Password-Based Cryptography September 2000 + + + EM = M || PS , + + where the padding string PS consists of some number psLen + octets each with value psLen, where psLen is between 1 and 8. + If it is not possible to separate the encoded message EM in + this manner, output "decryption error" and stop. + + 6. Output the recovered message M. + +6.2 PBES2 + + PBES2 combines a password-based key derivation function, which shall + be PBKDF2 (Section 5.2) for this version of PKCS #5, with an + underlying encryption scheme (see Appendix B.2 for examples). The key + length and any other parameters for the underlying encryption scheme + depend on the scheme. + + PBES2 is recommended for new applications. + +6.2.1 Encryption Operation + + The encryption operation for PBES2 consists of the following steps, + which encrypt a message M under a password P to produce a ciphertext + C, applying a selected key derivation function KDF and a selected + underlying encryption scheme: + + 1. Select a salt S and an iteration count c, as outlined in + Section 4. + + 2. Select the length in octets, dkLen, for the derived key for the + underlying encryption scheme. + + 3. Apply the selected key derivation function to the password P, + the salt S, and the iteration count c to produce a derived key + DK of length dkLen octets: + + DK = KDF (P, S, c, dkLen) . + + 4. Encrypt the message M with the underlying encryption scheme + under the derived key DK to produce a ciphertext C. (This step + may involve selection of parameters such as an initialization + vector and padding, depending on the underlying scheme.) + + 5. Output the ciphertext C. + + + + + + + +Kaliski Informational [Page 14] + +RFC 2898 Password-Based Cryptography September 2000 + + + The salt S, the iteration count c, the key length dkLen, and + identifiers for the key derivation function and the underlying + encryption scheme may be conveyed to the party performing decryption + in an AlgorithmIdentifier value (see Appendix A.4). + +6.2.2 Decryption Operation + + The decryption operation for PBES2 consists of the following steps, + which decrypt a ciphertext C under a password P to recover a message + M: + + 1. Obtain the salt S for the operation. + + 2. Obtain the iteration count c for the key derivation function. + + 3. Obtain the key length in octets, dkLen, for the derived key for + the underlying encryption scheme. + + 4. Apply the selected key derivation function to the password P, + the salt S, and the iteration count c to produce a derived key + DK of length dkLen octets: + + DK = KDF (P, S, c, dkLen) . + + 5. Decrypt the ciphertext C with the underlying encryption scheme + under the derived key DK to recover a message M. If the + decryption function outputs "decryption error," then output + "decryption error" and stop. + + 6. Output the recovered message M. + +7. Message Authentication Schemes + + A message authentication scheme consists of a MAC (message + authentication code) generation operation and a MAC verification + operation, where the MAC generation operation produces a message + authentication code from a message under a key, and the MAC + verification operation verifies the message authentication code under + the same key. In a password-based message authentication scheme, the + key is a password. + + One scheme is specified in this section: PBMAC1. + + + + + + + + + +Kaliski Informational [Page 15] + +RFC 2898 Password-Based Cryptography September 2000 + + +7.1 PBMAC1 + + PBMAC1 combines a password-based key derivation function, which shall + be PBKDF2 (Section 5.2) for this version of PKCS #5, with an + underlying message authentication scheme (see Appendix B.3 for an + example). The key length and any other parameters for the underlying + message authentication scheme depend on the scheme. + +7.1.1 MAC Generation + + The MAC generation operation for PBMAC1 consists of the following + steps, which process a message M under a password P to generate a + message authentication code T, applying a selected key derivation + function KDF and a selected underlying message authentication scheme: + + 1. Select a salt S and an iteration count c, as outlined in + Section 4. + + 2. Select a key length in octets, dkLen, for the derived key for + the underlying message authentication function. + + 3. Apply the selected key derivation function to the password P, + the salt S, and the iteration count c to produce a derived key + DK of length dkLen octets: + + DK = KDF (P, S, c, dkLen) . + + 4. Process the message M with the underlying message + authentication scheme under the derived key DK to generate a + message authentication code T. + + 5. Output the message authentication code T. + + The salt S, the iteration count c, the key length dkLen, and + identifiers for the key derivation function and underlying message + authentication scheme may be conveyed to the party performing + verification in an AlgorithmIdentifier value (see Appendix A.5). + +7.1.2 MAC Verification + + The MAC verification operation for PBMAC1 consists of the following + steps, which process a message M under a password P to verify a + message authentication code T: + + 1. Obtain the salt S and the iteration count c. + + 2. Obtain the key length in octets, dkLen, for the derived key for + the underlying message authentication scheme. + + + +Kaliski Informational [Page 16] + +RFC 2898 Password-Based Cryptography September 2000 + + + 3. Apply the selected key derivation function to the password P, + the salt S, and the iteration count c to produce a derived key + DK of length dkLen octets: + + DK = KDF (P, S, c, dkLen) . + + 4. Process the message M with the underlying message + authentication scheme under the derived key DK to verify the + message authentication code T. + + 5. If the message authentication code verifies, output "correct"; + else output "incorrect." + +8. Security Considerations + + Password-based cryptography is generally limited in the security that + it can provide, particularly for methods such as those defined in + this document where off-line password search is possible. While the + use of salt and iteration count can increase the complexity of attack + (see Section 4 for recommendations), it is essential that passwords + are selected well, and relevant guidelines (e.g., [17]) should be + taken into account. It is also important that passwords be protected + well if stored. + + In general, different keys should be derived from a password for + different uses to minimize the possibility of unintended + interactions. For password-based encryption with a single algorithm, + a random salt is sufficient to ensure that different keys will be + produced. In certain other situations, as outlined in Section 4, a + structured salt is necessary. The recommendations in Section 4 should + thus be taken into account when selecting the salt value. + +9. Author's Address + + Burt Kaliski + RSA Laboratories + 20 Crosby Drive + Bedford, MA 01730 USA + + EMail: bkaliski@rsasecurity.com + + + + + + + + + + + +Kaliski Informational [Page 17] + +RFC 2898 Password-Based Cryptography September 2000 + + +APPENDICES + +A. ASN.1 Syntax + + This section defines ASN.1 syntax for the key derivation functions, + the encryption schemes, the message authentication scheme, and + supporting techniques. The intended application of these definitions + includes PKCS #8 and other syntax for key management, encrypted data, + and integrity-protected data. (Various aspects of ASN.1 are specified + in several ISO/IEC standards [9][10][11][12][13][14].) + + The object identifier pkcs-5 identifies the arc of the OID tree from + which the PKCS #5-specific OIDs in this section are derived: + + rsadsi OBJECT IDENTIFIER ::= {iso(1) member-body(2) us(840) 113549} + pkcs OBJECT IDENTIFIER ::= {rsadsi 1} + pkcs-5 OBJECT IDENTIFIER ::= {pkcs 5} + +A.1 PBKDF1 + + No object identifier is given for PBKDF1, as the object identifiers + for PBES1 are sufficient for existing applications and PBKDF2 is + recommended for new applications. + +A.2 PBKDF2 + + The object identifier id-PBKDF2 identifies the PBKDF2 key derivation + function (Section 5.2). + + id-PBKDF2 OBJECT IDENTIFIER ::= {pkcs-5 12} + + The parameters field associated with this OID in an + AlgorithmIdentifier shall have type PBKDF2-params: + + PBKDF2-params ::= SEQUENCE { + salt CHOICE { + specified OCTET STRING, + otherSource AlgorithmIdentifier {{PBKDF2-SaltSources}} + }, + iterationCount INTEGER (1..MAX), + keyLength INTEGER (1..MAX) OPTIONAL, + prf AlgorithmIdentifier {{PBKDF2-PRFs}} DEFAULT + algid-hmacWithSHA1 } + + The fields of type PKDF2-params have the following meanings: + + + + + + +Kaliski Informational [Page 18] + +RFC 2898 Password-Based Cryptography September 2000 + + + - salt specifies the salt value, or the source of the salt value. + It shall either be an octet string or an algorithm ID with an OID + in the set PBKDF2-SaltSources, which is reserved for future + versions of PKCS #5. + + The salt-source approach is intended to indicate how the salt + value is to be generated as a function of parameters in the + algorithm ID, application data, or both. For instance, it may + indicate that the salt value is produced from the encoding of a + structure that specifies detailed information about the derived + key as suggested in Section 4.1. Some of the information may be + carried elsewhere, e.g., in the encryption algorithm ID. However, + such facilities are deferred to a future version of PKCS #5. + + In this version, an application may achieve the benefits mentioned + in Section 4.1 by choosing a particular interpretation of the salt + value in the specified alternative. + + PBKDF2-SaltSources ALGORITHM-IDENTIFIER ::= { ... } + + - iterationCount specifies the iteration count. The maximum + iteration count allowed depends on the implementation. It is + expected that implementation profiles may further constrain the + bounds. + + - keyLength, an optional field, is the length in octets of the + derived key. The maximum key length allowed depends on the + implementation; it is expected that implementation profiles may + further constrain the bounds. The field is provided for + convenience only; the key length is not cryptographically + protected. If there is concern about interaction between + operations with different key lengths for a given salt (see + Section 4.1), the salt should distinguish among the different key + lengths. + + - prf identifies the underlying pseudorandom function. It shall be + an algorithm ID with an OID in the set PBKDF2-PRFs, which for this + version of PKCS #5 shall consist of id-hmacWithSHA1 (see Appendix + B.1.1) and any other OIDs defined by the application. + + PBKDF2-PRFs ALGORITHM-IDENTIFIER ::= + { {NULL IDENTIFIED BY id-hmacWithSHA1}, ... } + + The default pseudorandom function is HMAC-SHA-1: + + algid-hmacWithSHA1 AlgorithmIdentifier {{PBKDF2-PRFs}} ::= + {algorithm id-hmacWithSHA1, parameters NULL : NULL} + + + + +Kaliski Informational [Page 19] + +RFC 2898 Password-Based Cryptography September 2000 + + +A.3 PBES1 + + Different object identifiers identify the PBES1 encryption scheme + (Section 6.1) according to the underlying hash function in the key + derivation function and the underlying block cipher, as summarized in + the following table: + + Hash Function Block Cipher OID + MD2 DES pkcs-5.1 + MD2 RC2 pkcs-5.4 + MD5 DES pkcs-5.3 + MD5 RC2 pkcs-5.6 + SHA-1 DES pkcs-5.10 + SHA-1 RC2 pkcs-5.11 + + pbeWithMD2AndDES-CBC OBJECT IDENTIFIER ::= {pkcs-5 1} + pbeWithMD2AndRC2-CBC OBJECT IDENTIFIER ::= {pkcs-5 4} + pbeWithMD5AndDES-CBC OBJECT IDENTIFIER ::= {pkcs-5 3} + pbeWithMD5AndRC2-CBC OBJECT IDENTIFIER ::= {pkcs-5 6} + pbeWithSHA1AndDES-CBC OBJECT IDENTIFIER ::= {pkcs-5 10} + pbeWithSHA1AndRC2-CBC OBJECT IDENTIFIER ::= {pkcs-5 11} + + For each OID, the parameters field associated with the OID in an + AlgorithmIdentifier shall have type PBEParameter: + + PBEParameter ::= SEQUENCE { + salt OCTET STRING (SIZE(8)), + iterationCount INTEGER } + + The fields of type PBEParameter have the following meanings: + + - salt specifies the salt value, an eight-octet string. + + - iterationCount specifies the iteration count. + +A.4 PBES2 + + The object identifier id-PBES2 identifies the PBES2 encryption scheme + (Section 6.2). + + id-PBES2 OBJECT IDENTIFIER ::= {pkcs-5 13} + + The parameters field associated with this OID in an + AlgorithmIdentifier shall have type PBES2-params: + + PBES2-params ::= SEQUENCE { + keyDerivationFunc AlgorithmIdentifier {{PBES2-KDFs}}, + encryptionScheme AlgorithmIdentifier {{PBES2-Encs}} } + + + +Kaliski Informational [Page 20] + +RFC 2898 Password-Based Cryptography September 2000 + + + The fields of type PBES2-params have the following meanings: + + - keyDerivationFunc identifies the underlying key derivation + function. It shall be an algorithm ID with an OID in the set + PBES2-KDFs, which for this version of PKCS #5 shall consist of + id-PBKDF2 (Appendix A.2). + + PBES2-KDFs ALGORITHM-IDENTIFIER ::= + { {PBKDF2-params IDENTIFIED BY id-PBKDF2}, ... } + + - encryptionScheme identifies the underlying encryption scheme. It + shall be an algorithm ID with an OID in the set PBES2-Encs, whose + definition is left to the application. Example underlying + encryption schemes are given in Appendix B.2. + + PBES2-Encs ALGORITHM-IDENTIFIER ::= { ... } + +A.5 PBMAC1 + + The object identifier id-PBMAC1 identifies the PBMAC1 message + authentication scheme (Section 7.1). + + id-PBMAC1 OBJECT IDENTIFIER ::= {pkcs-5 14} + + The parameters field associated with this OID in an + AlgorithmIdentifier shall have type PBMAC1-params: + + PBMAC1-params ::= SEQUENCE { + keyDerivationFunc AlgorithmIdentifier {{PBMAC1-KDFs}}, + messageAuthScheme AlgorithmIdentifier {{PBMAC1-MACs}} } + + The keyDerivationFunc field has the same meaning as the corresponding + field of PBES2-params (Appendix A.4) except that the set of OIDs is + PBMAC1-KDFs. + + PBMAC1-KDFs ALGORITHM-IDENTIFIER ::= + { {PBKDF2-params IDENTIFIED BY id-PBKDF2}, ... } + + The messageAuthScheme field identifies the underlying message + authentication scheme. It shall be an algorithm ID with an OID in the + set PBMAC1-MACs, whose definition is left to the application. Example + underlying encryption schemes are given in Appendix B.3. + + PBMAC1-MACs ALGORITHM-IDENTIFIER ::= { ... } + + + + + + + +Kaliski Informational [Page 21] + +RFC 2898 Password-Based Cryptography September 2000 + + +B. Supporting Techniques + + This section gives several examples of underlying functions and + schemes supporting the password-based schemes in Sections 5, 6 and 7. + + While these supporting techniques are appropriate for applications to + implement, none of them is required to be implemented. It is + expected, however, that profiles for PKCS #5 will be developed that + specify particular supporting techniques. + + This section also gives object identifiers for the supporting + techniques. The object identifiers digestAlgorithm and + encryptionAlgorithm identify the arcs from which certain algorithm + OIDs referenced in this section are derived: + + digestAlgorithm OBJECT IDENTIFIER ::= {rsadsi 2} + encryptionAlgorithm OBJECT IDENTIFIER ::= {rsadsi 3} + +B.1 Pseudorandom functions + + An example pseudorandom function for PBKDF2 (Section 5.2) is HMAC- + SHA-1. + +B.1.1 HMAC-SHA-1 + + HMAC-SHA-1 is the pseudorandom function corresponding to the HMAC + message authentication code [7] based on the SHA-1 hash function + [18]. The pseudorandom function is the same function by which the + message authentication code is computed, with a full-length output. + (The first argument to the pseudorandom function PRF serves as HMAC's + "key," and the second serves as HMAC's "text." In the case of PBKDF2, + the "key" is thus the password and the "text" is the salt.) HMAC- + SHA-1 has a variable key length and a 20-octet (160-bit) output + value. + + Although the length of the key to HMAC-SHA-1 is essentially + unbounded, the effective search space for pseudorandom function + outputs may be limited by the structure of the function. In + particular, when the key is longer than 512 bits, HMAC-SHA-1 will + first hash it to 160 bits. Thus, even if a long derived key + consisting of several pseudorandom function outputs is produced from + a key, the effective search space for the derived key will be at most + 160 bits. Although the specific limitation for other key sizes + depends on details of the HMAC construction, one should assume, to be + conservative, that the effective search space is limited to 160 bits + for other key sizes as well. + + + + + +Kaliski Informational [Page 22] + +RFC 2898 Password-Based Cryptography September 2000 + + + (The 160-bit limitation should not generally pose a practical + limitation in the case of password-based cryptography, since the + search space for a password is unlikely to be greater than 160 bits.) + + The object identifier id-hmacWithSHA1 identifies the HMAC-SHA-1 + pseudorandom function: + + id-hmacWithSHA1 OBJECT IDENTIFIER ::= {digestAlgorithm 7} + + The parameters field associated with this OID in an + AlgorithmIdentifier shall have type NULL. This object identifier is + employed in the object set PBKDF2-PRFs (Appendix A.2). + + Note. Although HMAC-SHA-1 was designed as a message authentication + code, its proof of security is readily modified to accommodate + requirements for a pseudorandom function, under stronger assumptions. + + A hash function may also meet the requirements of a pseudorandom + function under certain assumptions. For instance, the direct + application of a hash function to to the concatenation of the "key" + and the "text" may be appropriate, provided that "text" has + appropriate structure to prevent certain attacks. HMAC-SHA-1 is + preferable, however, because it treats "key" and "text" as separate + arguments and does not require "text" to have any structure. + +B.2 Encryption Schemes + + Example pseudorandom functions for PBES2 (Section 6.2) are DES-CBC- + Pad, DES-EDE2-CBC-Pad, RC2-CBC-Pad, and RC5-CBC-Pad. + + The object identifiers given in this section are intended to be + employed in the object set PBES2-Encs (Appendix A.4). + +B.2.1 DES-CBC-Pad + + DES-CBC-Pad is single-key DES [15] in CBC mode [16] with the RFC 1423 + padding operation (see Section 6.1.1). DES-CBC-Pad has an eight-octet + encryption key and an eight-octet initialization vector. The key is + considered as a 64-bit encoding of a 56-bit DES key with parity bits + ignored. + + The object identifier desCBC (defined in the NIST/OSI Implementors' + Workshop agreements) identifies the DES-CBC-Pad encryption scheme: + + desCBC OBJECT IDENTIFIER ::= + {iso(1) identified-organization(3) oiw(14) secsig(3) + algorithms(2) 7} + + + + +Kaliski Informational [Page 23] + +RFC 2898 Password-Based Cryptography September 2000 + + + The parameters field associated with this OID in an + AlgorithmIdentifier shall have type OCTET STRING (SIZE(8)), + specifying the initialization vector for CBC mode. + +B.2.2 DES-EDE3-CBC-Pad + + DES-EDE3-CBC-Pad is three-key triple-DES in CBC mode [1] with the RFC + 1423 padding operation. DES-EDE3-CBC-Pad has a 24-octet encryption + key and an eight-octet initialization vector. The key is considered + as the concatenation of three eight-octet keys, each of which is a + 64-bit encoding of a 56-bit DES key with parity bits ignored. + + The object identifier des-EDE3-CBC identifies the DES-EDE3-CBC-Pad + encryption scheme: + + des-EDE3-CBC OBJECT IDENTIFIER ::= {encryptionAlgorithm 7} + + The parameters field associated with this OID in an + AlgorithmIdentifier shall have type OCTET STRING (SIZE(8)), + specifying the initialization vector for CBC mode. + + Note. An OID for DES-EDE3-CBC without padding is given in ANSI X9.52 + [1]; the one given here is preferred since it specifies padding. + +B.2.3 RC2-CBC-Pad + + RC2-CBC-Pad is the RC2(tm) encryption algorithm [21] in CBC mode with + the RFC 1423 padding operation. RC2-CBC-Pad has a variable key + length, from one to 128 octets, a separate "effective key bits" + parameter from one to 1024 bits that limits the effective search + space independent of the key length, and an eight-octet + initialization vector. + + The object identifier rc2CBC identifies the RC2-CBC-Pad encryption + scheme: + + rc2CBC OBJECT IDENTIFIER ::= {encryptionAlgorithm 2} + + The parameters field associated with OID in an AlgorithmIdentifier + shall have type RC2-CBC-Parameter: + + RC2-CBC-Parameter ::= SEQUENCE { + rc2ParameterVersion INTEGER OPTIONAL, + iv OCTET STRING (SIZE(8)) } + + + + + + + +Kaliski Informational [Page 24] + +RFC 2898 Password-Based Cryptography September 2000 + + + The fields of type RC2-CBCParameter have the following meanings: + + - rc2ParameterVersion is a proprietary RSA Security Inc. encoding of + the "effective key bits" for RC2. The following encodings are + defined: + + Effective Key Bits Encoding + 40 160 + 64 120 + 128 58 + b >= 256 b + + If the rc2ParameterVersion field is omitted, the "effective key bits" + defaults to 32. (This is for backward compatibility with certain very + old implementations.) + + - iv is the eight-octet initialization vector. + +B.2.4 RC5-CBC-Pad + + RC5-CBC-Pad is the RC5(tm) encryption algorithm [20] in CBC mode with + a generalization of the RFC 1423 padding operation. This scheme is + fully specified in [2]. RC5-CBC-Pad has a variable key length, from 0 + to 256 octets, and supports both a 64-bit block size and a 128-bit + block size. For the former, it has an eight-octet initialization + vector, and for the latter, a 16-octet initialization vector. + RC5-CBC-Pad also has a variable number of "rounds" in the encryption + operation, from 8 to 127. + + Note: The generalization of the padding operation is as follows. For + RC5 with a 64-bit block size, the padding string is as defined in RFC + 1423. For RC5 with a 128-bit block size, the padding string consists + of 16-(||M|| mod 16) octets each with value 16-(||M|| mod 16). + + The object identifier rc5-CBC-PAD [2] identifies RC5-CBC-Pad + encryption scheme: + + rc5-CBC-PAD OBJECT IDENTIFIER ::= {encryptionAlgorithm 9} + + The parameters field associated with this OID in an + AlgorithmIdentifier shall have type RC5-CBC-Parameters: + + RC5-CBC-Parameters ::= SEQUENCE { + version INTEGER {v1-0(16)} (v1-0), + rounds INTEGER (8..127), + blockSizeInBits INTEGER (64 | 128), + iv OCTET STRING OPTIONAL } + + + + +Kaliski Informational [Page 25] + +RFC 2898 Password-Based Cryptography September 2000 + + + The fields of type RC5-CBC-Parameters have the following meanings: + + - version is the version of the algorithm, which shall be v1-0. + + - rounds is the number of rounds in the encryption operation, which + shall be between 8 and 127. + + - blockSizeInBits is the block size in bits, which shall be 64 or + 128. + + - iv is the initialization vector, an eight-octet string for 64-bit + RC5 and a 16-octet string for 128-bit RC5. The default is a string + of the appropriate length consisting of zero octets. + +B.3 Message Authentication Schemes + + An example message authentication scheme for PBMAC1 (Section 7.1) is + HMAC-SHA-1. + +B.3.1 HMAC-SHA-1 + + HMAC-SHA-1 is the HMAC message authentication scheme [7] based on the + SHA-1 hash function [18]. HMAC-SHA-1 has a variable key length and a + 20-octet (160-bit) message authentication code. + + The object identifier id-hmacWithSHA1 (see Appendix B.1.1) identifies + the HMAC-SHA-1 message authentication scheme. (The object identifier + is the same for both the pseudorandom function and the message + authentication scheme; the distinction is to be understood by + context.) This object identifier is intended to be employed in the + object set PBMAC1-Macs (Appendix A.5). + +C. ASN.1 Module + + For reference purposes, the ASN.1 syntax in the preceding sections is + presented as an ASN.1 module here. + + -- PKCS #5 v2.0 ASN.1 Module + -- Revised March 25, 1999 + + -- This module has been checked for conformance with the + -- ASN.1 standard by the OSS ASN.1 Tools + + PKCS5v2-0 {iso(1) member-body(2) us(840) rsadsi(113549) + pkcs(1) pkcs-5(5) modules(16) pkcs5v2-0(1)} + + DEFINITIONS ::= BEGIN + + + + +Kaliski Informational [Page 26] + +RFC 2898 Password-Based Cryptography September 2000 + + + -- Basic object identifiers + + rsadsi OBJECT IDENTIFIER ::= {iso(1) member-body(2) us(840) 113549} + pkcs OBJECT IDENTIFIER ::= {rsadsi 1} + + pkcs-5 OBJECT IDENTIFIER ::= {pkcs 5} + + -- Basic types and classes + + AlgorithmIdentifier { ALGORITHM-IDENTIFIER:InfoObjectSet } ::= + SEQUENCE { + algorithm ALGORITHM-IDENTIFIER.&id({InfoObjectSet}), + parameters ALGORITHM-IDENTIFIER.&Type({InfoObjectSet} + {@algorithm}) OPTIONAL + } + + ALGORITHM-IDENTIFIER ::= TYPE-IDENTIFIER + + -- PBKDF2 + + PBKDF2Algorithms ALGORITHM-IDENTIFIER ::= + { {PBKDF2-params IDENTIFIED BY id-PBKDF2}, ...} + + id-PBKDF2 OBJECT IDENTIFIER ::= {pkcs-5 12} + + algid-hmacWithSHA1 AlgorithmIdentifier {{PBKDF2-PRFs}} ::= + {algorithm id-hmacWithSHA1, parameters NULL : NULL} + + PBKDF2-params ::= SEQUENCE { + salt CHOICE { + specified OCTET STRING, + otherSource AlgorithmIdentifier {{PBKDF2-SaltSources}} + }, + iterationCount INTEGER (1..MAX), + keyLength INTEGER (1..MAX) OPTIONAL, + prf AlgorithmIdentifier {{PBKDF2-PRFs}} DEFAULT + algid-hmacWithSHA1 + } + + PBKDF2-SaltSources ALGORITHM-IDENTIFIER ::= { ... } + + PBKDF2-PRFs ALGORITHM-IDENTIFIER ::= + { {NULL IDENTIFIED BY id-hmacWithSHA1}, ... } + + -- PBES1 + + + PBES1Algorithms ALGORITHM-IDENTIFIER ::= { + + + +Kaliski Informational [Page 27] + +RFC 2898 Password-Based Cryptography September 2000 + + + {PBEParameter IDENTIFIED BY pbeWithMD2AndDES-CBC} | + {PBEParameter IDENTIFIED BY pbeWithMD2AndRC2-CBC} | + {PBEParameter IDENTIFIED BY pbeWithMD5AndDES-CBC} | + {PBEParameter IDENTIFIED BY pbeWithMD5AndRC2-CBC} | + {PBEParameter IDENTIFIED BY pbeWithSHA1AndDES-CBC} | + {PBEParameter IDENTIFIED BY pbeWithSHA1AndRC2-CBC}, + ... + } + + pbeWithMD2AndDES-CBC OBJECT IDENTIFIER ::= {pkcs-5 1} + pbeWithMD2AndRC2-CBC OBJECT IDENTIFIER ::= {pkcs-5 4} + pbeWithMD5AndDES-CBC OBJECT IDENTIFIER ::= {pkcs-5 3} + pbeWithMD5AndRC2-CBC OBJECT IDENTIFIER ::= {pkcs-5 6} + pbeWithSHA1AndDES-CBC OBJECT IDENTIFIER ::= {pkcs-5 10} + pbeWithSHA1AndRC2-CBC OBJECT IDENTIFIER ::= {pkcs-5 11} + + PBEParameter ::= SEQUENCE { + salt OCTET STRING (SIZE(8)), + iterationCount INTEGER + } + + -- PBES2 + + PBES2Algorithms ALGORITHM-IDENTIFIER ::= + { {PBES2-params IDENTIFIED BY id-PBES2}, ...} + + id-PBES2 OBJECT IDENTIFIER ::= {pkcs-5 13} + + PBES2-params ::= SEQUENCE { + keyDerivationFunc AlgorithmIdentifier {{PBES2-KDFs}}, + encryptionScheme AlgorithmIdentifier {{PBES2-Encs}} + } + + PBES2-KDFs ALGORITHM-IDENTIFIER ::= + { {PBKDF2-params IDENTIFIED BY id-PBKDF2}, ... } + + PBES2-Encs ALGORITHM-IDENTIFIER ::= { ... } + + -- PBMAC1 + + PBMAC1Algorithms ALGORITHM-IDENTIFIER ::= + { {PBMAC1-params IDENTIFIED BY id-PBMAC1}, ...} + + id-PBMAC1 OBJECT IDENTIFIER ::= {pkcs-5 14} + + PBMAC1-params ::= SEQUENCE { + keyDerivationFunc AlgorithmIdentifier {{PBMAC1-KDFs}}, + messageAuthScheme AlgorithmIdentifier {{PBMAC1-MACs}} + + + +Kaliski Informational [Page 28] + +RFC 2898 Password-Based Cryptography September 2000 + + + } + + PBMAC1-KDFs ALGORITHM-IDENTIFIER ::= + { {PBKDF2-params IDENTIFIED BY id-PBKDF2}, ... } + + PBMAC1-MACs ALGORITHM-IDENTIFIER ::= { ... } + + -- Supporting techniques + + digestAlgorithm OBJECT IDENTIFIER ::= {rsadsi 2} + encryptionAlgorithm OBJECT IDENTIFIER ::= {rsadsi 3} + + SupportingAlgorithms ALGORITHM-IDENTIFIER ::= { + {NULL IDENTIFIED BY id-hmacWithSHA1} | + {OCTET STRING (SIZE(8)) IDENTIFIED BY desCBC} | + {OCTET STRING (SIZE(8)) IDENTIFIED BY des-EDE3-CBC} | + {RC2-CBC-Parameter IDENTIFIED BY rc2CBC} | + {RC5-CBC-Parameters IDENTIFIED BY rc5-CBC-PAD}, + ... + } + + id-hmacWithSHA1 OBJECT IDENTIFIER ::= {digestAlgorithm 7} + + desCBC OBJECT IDENTIFIER ::= + {iso(1) identified-organization(3) oiw(14) secsig(3) + algorithms(2) 7} -- from OIW + + des-EDE3-CBC OBJECT IDENTIFIER ::= {encryptionAlgorithm 7} + + rc2CBC OBJECT IDENTIFIER ::= {encryptionAlgorithm 2} + + RC2-CBC-Parameter ::= SEQUENCE { + rc2ParameterVersion INTEGER OPTIONAL, + iv OCTET STRING (SIZE(8)) + } + + rc5-CBC-PAD OBJECT IDENTIFIER ::= {encryptionAlgorithm 9} + + RC5-CBC-Parameters ::= SEQUENCE { + version INTEGER {v1-0(16)} (v1-0), + rounds INTEGER (8..127), + blockSizeInBits INTEGER (64 | 128), + iv OCTET STRING OPTIONAL + } + + END + + + + + +Kaliski Informational [Page 29] + +RFC 2898 Password-Based Cryptography September 2000 + + +Intellectual Property Considerations + + RSA Security makes no patent claims on the general constructions + described in this document, although specific underlying techniques + may be covered. Among the underlying techniques, the RC5 encryption + algorithm (Appendix B.2.4) is protected by U.S. Patents 5,724,428 + [22] and 5,835,600 [23]. + + RC2 and RC5 are trademarks of RSA Security. + + License to copy this document is granted provided that it is + identified as RSA Security Inc. Public-Key Cryptography Standards + (PKCS) in all material mentioning or referencing this document. + + RSA Security makes no representations regarding intellectual property + claims by other parties. Such determination is the responsibility of + the user. + +Revision history + + Versions 1.0-1.3 + + Versions 1.0-1.3 were distributed to participants in RSA Data + Security Inc.'s Public-Key Cryptography Standards meetings in + February and March 1991. + + Version 1.4 + + Version 1.4 was part of the June 3, 1991 initial public release of + PKCS. Version 1.4 was published as NIST/OSI Implementors' Workshop + document SEC-SIG-91-20. + + Version 1.5 + + Version 1.5 incorporated several editorial changes, including + updates to the references and the addition of a revision history. + + Version 2.0 + + Version 2.0 incorporates major editorial changes in terms of the + document structure, and introduces the PBES2 encryption scheme, + the PBMAC1 message authentication scheme, and independent + password-based key derivation functions. This version continues to + support the encryption process in version 1.5. + + + + + + + +Kaliski Informational [Page 30] + +RFC 2898 Password-Based Cryptography September 2000 + + +References + + [1] American National Standard X9.52 - 1998, Triple Data Encryption + Algorithm Modes of Operation. Working draft, Accredited + Standards Committee X9, July 27, 1998. + + [2] Baldwin, R. and R. Rivest, "The RC5, RC5-CBC, RC5-CBC-Pad, and + RC5-CTS Algorithms", RFC 2040, October 1996. + + [3] Balenson, D., "Privacy Enhancement for Internet Electronic Mail: + Part III: Algorithms, Modes, and Identifiers", RFC 1423, + February 1993. + + [4] S.M. Bellovin and M. Merritt. Encrypted key exchange: + Password-based protocols secure against dictionary attacks. In + Proceedings of the 1992 IEEE Computer Society Conference on + Research in Security and Privacy, pages 72-84, IEEE Computer + Society, 1992. + + [5] D. Jablon. Strong password-only authenticated key exchange. ACM + Computer Communications Review, October 1996. + + [6] Kaliski, B., "The MD2 Message-Digest Algorithm", RFC 1319, April + 1992. + + [7] Krawczyk, H., Bellare, M. and R. Canetti, "HMAC: Keyed-Hashing + for Message Authentication", RFC 2104, February 1997. + + [8] Robert Morris and Ken Thompson. Password security: A case + history. Communications of the ACM, 22(11):594-597, November + 1979. + + [9] ISO/IEC 8824-1:1995: Information technology - Abstract Syntax + Notation One (ASN.1) - Specification of basic notation. 1995. + + [10] ISO/IEC 8824-1:1995/Amd.1:1995 Information technology - Abstract + Syntax Notation One (ASN.1) - Specification of basic notation - + Amendment 1 - Rules of extensibility. 1995. + + [11] ISO/IEC 8824-2:1995 Information technology - Abstract Syntax + Notation One (ASN.1) - Information object specification. 1995. + + [12] ISO/IEC 8824-2:1995/Amd.1:1995 Information technology - Abstract + Syntax Notation One (ASN.1) - Information object specification - + Amendment 1 - Rules of extensibility. 1995. + + [13] ISO/IEC 8824-3:1995 Information technology - Abstract Syntax + Notation One (ASN.1) - Constraint specification. 1995. + + + +Kaliski Informational [Page 31] + +RFC 2898 Password-Based Cryptography September 2000 + + + [14] ISO/IEC 8824-4:1995 Information technology - Abstract Syntax + Notation One (ASN.1) - Parameterization of ASN.1 specifications. + 1995. + + [15] National Institute of Standards and Technology (NIST). FIPS PUB + 46-2: Data Encryption Standard. December 30, 1993. + + [16] National Institute of Standards and Technology (NIST). FIPS PUB + 81: DES Modes of Operation. December 2, 1980. + + [17] National Institute of Standards and Technology (NIST). FIPS PUB + 112: Password Usage. May 30, 1985. + + [18] National Institute of Standards and Technology (NIST). FIPS PUB + 180-1: Secure Hash Standard. April 1994. + + [19] Rivest, R., "The MD5 Message-Digest Algorithm", RFC 1321, April + 1992. + + [20] R.L. Rivest. The RC5 encryption algorithm. In Proceedings of the + Second International Workshop on Fast Software Encryption, pages + 86-96, Springer-Verlag, 1994. + + [21] Rivest, R., "A Description of the RC2(r) Encryption Algorithm", + RFC 2268, March 1998. + + [22] R.L. Rivest. Block-Encryption Algorithm with Data-Dependent + Rotations. U.S. Patent No. 5,724,428, March 3, 1998. + + [23] R.L. Rivest. Block Encryption Algorithm with Data-Dependent + Rotations. U.S. Patent No. 5,835,600, November 10, 1998. + + [24] RSA Laboratories. PKCS #5: Password-Based Encryption Standard. + Version 1.5, November 1993. + + [25] RSA Laboratories. PKCS #8: Private-Key Information Syntax + Standard. Version 1.2, November 1993. + + [26] T. Wu. The Secure Remote Password protocol. In Proceedings of + the 1998 Internet Society Network and Distributed System + Security Symposium, pages 97-111, Internet Society, 1998. + + [27] Yergeau, F., "UTF-8, a transformation format of ISO 10646", RFC + 2279, January 1998. + + + + + + + +Kaliski Informational [Page 32] + +RFC 2898 Password-Based Cryptography September 2000 + + +Contact Information & About PKCS + + The Public-Key Cryptography Standards are specifications produced by + RSA Laboratories in cooperation with secure systems developers + worldwide for the purpose of accelerating the deployment of public- + key cryptography. First published in 1991 as a result of meetings + with a small group of early adopters of public-key technology, the + PKCS documents have become widely referenced and implemented. + Contributions from the PKCS series have become part of many formal + and de facto standards, including ANSI X9 documents, PKIX, SET, + S/MIME, and SSL. + + Further development of PKCS occurs through mailing list discussions + and occasional workshops, and suggestions for improvement are + welcome. For more information, contact: + + PKCS Editor + RSA Laboratories + 20 Crosby Drive + Bedford, MA 01730 USA + pkcs-editor@rsasecurity.com + http://www.rsalabs.com/pkcs/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Kaliski Informational [Page 33] + +RFC 2898 Password-Based Cryptography September 2000 + + +Full Copyright Statement + + Copyright (C) The Internet Society (2000). All Rights Reserved. + + This document and translations of it may be copied and furnished to + others, and derivative works that comment on or otherwise explain it + or assist in its implementation may be prepared, copied, published + and distributed, in whole or in part, without restriction of any + kind, provided that the above copyright notice and this paragraph are + included on all such copies and derivative works. However, this + document itself may not be modified in any way, such as by removing + the copyright notice or references to the Internet Society or other + Internet organizations, except as needed for the purpose of + developing Internet standards in which case the procedures for + copyrights defined in the Internet Standards process must be + followed, or as required to translate it into languages other than + English. + + The limited permissions granted above are perpetual and will not be + revoked by the Internet Society or its successors or assigns. + + This document and the information contained herein is provided on an + "AS IS" basis and THE INTERNET SOCIETY AND THE INTERNET ENGINEERING + TASK FORCE DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING + BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION + HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED WARRANTIES OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + +Acknowledgement + + Funding for the RFC Editor function is currently provided by the + Internet Society. + + + + + + + + + + + + + + + + + + + +Kaliski Informational [Page 34] + diff --git a/src/common/libManageSieve/doc/SASL SCRAM/PKCS #5/rfc6070.txt b/src/common/libManageSieve/doc/SASL SCRAM/PKCS #5/rfc6070.txt new file mode 100644 index 00000000..67cd340b --- /dev/null +++ b/src/common/libManageSieve/doc/SASL SCRAM/PKCS #5/rfc6070.txt @@ -0,0 +1,283 @@ + + + + + + +Internet Engineering Task Force (IETF) S. Josefsson +Request for Comments: 6070 SJD AB +Category: Informational January 2011 +ISSN: 2070-1721 + + + PKCS #5: Password-Based Key Derivation Function 2 (PBKDF2) + Test Vectors + +Abstract + + This document contains test vectors for the Public-Key Cryptography + Standards (PKCS) #5 Password-Based Key Derivation Function 2 (PBKDF2) + with the Hash-based Message Authentication Code (HMAC) Secure Hash + Algorithm (SHA-1) pseudorandom function. + +Status of This Memo + + This document is not an Internet Standards Track specification; it is + published for informational purposes. + + This document is a product of the Internet Engineering Task Force + (IETF). It represents the consensus of the IETF community. It has + received public review and has been approved for publication by the + Internet Engineering Steering Group (IESG). Not all documents + approved by the IESG are a candidate for any level of Internet + Standard; see Section 2 of RFC 5741. + + Information about the current status of this document, any errata, + and how to provide feedback on it may be obtained at + http://www.rfc-editor.org/info/rfc6070. + +Copyright Notice + + Copyright (c) 2011 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents + (http://trustee.ietf.org/license-info) in effect on the date of + publication of this document. Please review these documents + carefully, as they describe your rights and restrictions with respect + to this document. Code Components extracted from this document must + include Simplified BSD License text as described in Section 4.e of + the Trust Legal Provisions and are provided without warranty as + described in the Simplified BSD License. + + + + + +Josefsson Informational [Page 1] + +RFC 6070 PKCS #5 PBKDF2 Test Vectors January 2011 + + +Table of Contents + + 1. Introduction ....................................................2 + 2. PBKDF2 HMAC-SHA1 Test Vectors ...................................2 + 3. Acknowledgements ................................................4 + 4. Copying Conditions ..............................................4 + 5. Security Considerations .........................................4 + 6. References ......................................................4 + 6.1. Normative References .......................................4 + 6.2. Informative References .....................................5 + +1. Introduction + + The Public-Key Cryptography Standards (PKCS) #5 [RFC2898] Password- + Based Key Derivation Function 2 (PBKDF2) is used by several protocols + to derive encryption keys from a password. + + For example, Salted Challenge Response Authentication Mechanism + (SCRAM) [RFC5802] uses PBKDF2 with Hash-based Message Authentication + Code (HMAC) [RFC2104] and Secure Hash Algorithm (SHA-1) + [FIPS.180-1.1995]. + + Test vectors for the algorithm were not included in the original + specification, but are often useful for implementers. This document + addresses the shortcoming. + +2. PBKDF2 HMAC-SHA1 Test Vectors + + The input strings below are encoded using ASCII [ANSI.X3-4.1986]. + The sequence "\0" (without quotation marks) means a literal ASCII NUL + value (1 octet). "DK" refers to the Derived Key. + + Input: + P = "password" (8 octets) + S = "salt" (4 octets) + c = 1 + dkLen = 20 + + Output: + DK = 0c 60 c8 0f 96 1f 0e 71 + f3 a9 b5 24 af 60 12 06 + 2f e0 37 a6 (20 octets) + + + + + + + + + +Josefsson Informational [Page 2] + +RFC 6070 PKCS #5 PBKDF2 Test Vectors January 2011 + + + Input: + P = "password" (8 octets) + S = "salt" (4 octets) + c = 2 + dkLen = 20 + + Output: + DK = ea 6c 01 4d c7 2d 6f 8c + cd 1e d9 2a ce 1d 41 f0 + d8 de 89 57 (20 octets) + + + Input: + P = "password" (8 octets) + S = "salt" (4 octets) + c = 4096 + dkLen = 20 + + Output: + DK = 4b 00 79 01 b7 65 48 9a + be ad 49 d9 26 f7 21 d0 + 65 a4 29 c1 (20 octets) + + + Input: + P = "password" (8 octets) + S = "salt" (4 octets) + c = 16777216 + dkLen = 20 + + Output: + DK = ee fe 3d 61 cd 4d a4 e4 + e9 94 5b 3d 6b a2 15 8c + 26 34 e9 84 (20 octets) + + + Input: + P = "passwordPASSWORDpassword" (24 octets) + S = "saltSALTsaltSALTsaltSALTsaltSALTsalt" (36 octets) + c = 4096 + dkLen = 25 + + Output: + DK = 3d 2e ec 4f e4 1c 84 9b + 80 c8 d8 36 62 c0 e4 4a + 8b 29 1a 96 4c f2 f0 70 + 38 (25 octets) + + + + +Josefsson Informational [Page 3] + +RFC 6070 PKCS #5 PBKDF2 Test Vectors January 2011 + + + Input: + P = "pass\0word" (9 octets) + S = "sa\0lt" (5 octets) + c = 4096 + dkLen = 16 + + Output: + DK = 56 fa 6a a7 55 48 09 9d + cc 37 d7 f0 34 25 e0 c3 (16 octets) + +3. Acknowledgements + + Barry Brachman and Love Hornquist Astrand confirmed the test vectors + (using independent implementations) and pointed out a mistake in the + salt octet length count. + +4. Copying Conditions + + This document should be considered a Code Component and is thus + available under the BSD license. + +5. Security Considerations + + The security considerations in [RFC2898] apply. This document does + not introduce any new security considerations. + +6. References + +6.1. Normative References + + [ANSI.X3-4.1986] + American National Standards Institute, "Coded Character + Set - 7-bit American Standard Code for Information + Interchange", ANSI X3.4, 1986. + + [RFC2104] Krawczyk, H., Bellare, M., and R. Canetti, "HMAC: Keyed- + Hashing for Message Authentication", RFC 2104, + February 1997. + + [RFC2898] Kaliski, B., "PKCS #5: Password-Based Cryptography + Specification Version 2.0", RFC 2898, September 2000. + + [FIPS.180-1.1995] + National Institute of Standards and Technology, "Secure + Hash Standard", FIPS PUB 180-1, April 1995, + <http://www.itl.nist.gov/fipspubs/fip180-1.htm>. + + + + + +Josefsson Informational [Page 4] + +RFC 6070 PKCS #5 PBKDF2 Test Vectors January 2011 + + +6.2. Informative References + + [RFC5802] Newman, C., Menon-Sen, A., Melnikov, A., and N. Williams, + "Salted Challenge Response Authentication Mechanism + (SCRAM) SASL and GSS-API Mechanisms", RFC 5802, + July 2010. + +Author's Address + + Simon Josefsson + SJD AB + Hagagatan 24 + Stockholm 113 47 + SE + + EMail: simon@josefsson.org + URI: http://josefsson.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Josefsson Informational [Page 5] + diff --git a/src/common/libManageSieve/doc/SASL SCRAM/draft-melnikov-scram-sha-512-01.txt b/src/common/libManageSieve/doc/SASL SCRAM/draft-melnikov-scram-sha-512-01.txt new file mode 100644 index 00000000..307cd877 --- /dev/null +++ b/src/common/libManageSieve/doc/SASL SCRAM/draft-melnikov-scram-sha-512-01.txt @@ -0,0 +1,392 @@ + + + + +Network Working Group A. Melnikov, Ed. +Internet-Draft Isode Ltd +Intended status: Standards Track November 18, 2020 +Expires: May 22, 2021 + + +SCRAM-SHA-512 and SCRAM-SHA-512-PLUS Simple Authentication and Security + Layer (SASL) Mechanisms + draft-melnikov-scram-sha-512-01 + +Abstract + + This document registers the Simple Authentication and Security Layer + (SASL) mechanisms SCRAM-SHA-512 and SCRAM-SHA-512-PLUS. + +Status of This Memo + + This Internet-Draft is submitted in full conformance with the + provisions of BCP 78 and BCP 79. + + Internet-Drafts are working documents of the Internet Engineering + Task Force (IETF). Note that other groups may also distribute + working documents as Internet-Drafts. The list of current Internet- + Drafts is at https://datatracker.ietf.org/drafts/current/. + + Internet-Drafts are draft documents valid for a maximum of six months + and may be updated, replaced, or obsoleted by other documents at any + time. It is inappropriate to use Internet-Drafts as reference + material or to cite them other than as "work in progress." + + This Internet-Draft will expire on May 22, 2021. + +Copyright Notice + + Copyright (c) 2020 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents + (https://trustee.ietf.org/license-info) in effect on the date of + publication of this document. Please review these documents + carefully, as they describe your rights and restrictions with respect + to this document. Code Components extracted from this document must + include Simplified BSD License text as described in Section 4.e of + the Trust Legal Provisions and are provided without warranty as + described in the Simplified BSD License. + + + + + +Melnikov Expires May 22, 2021 [Page 1] + +Internet-Draft SASL SCRAM-SHA-512/SCRAM-SHA-512-PLUS November 2020 + + +Table of Contents + + 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 2 + 2. Key Word Definitions . . . . . . . . . . . . . . . . . . . . 2 + 3. SCRAM-SHA-512 and SCRAM-SHA-512-PLUS . . . . . . . . . . . . 2 + 4. Security Considerations . . . . . . . . . . . . . . . . . . . 3 + 5. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 3 + 6. References . . . . . . . . . . . . . . . . . . . . . . . . . 4 + 6.1. Normative References . . . . . . . . . . . . . . . . . . 4 + 6.2. Informative References . . . . . . . . . . . . . . . . . 5 + Acknowledgements . . . . . . . . . . . . . . . . . . . . . . . . 7 + Author's Address . . . . . . . . . . . . . . . . . . . . . . . . 7 + +1. Introduction + + This document registers the SASL [RFC4422] mechanisms SCRAM-SHA-512 + and SCRAM-SHA-512-PLUS. SHA-512 has stronger security properties + than SHA-1, and it is expected that SCRAM mechanisms based on it will + have greater predicted longevity than the SCRAM mechanisms based on + SHA-1. + +2. Key Word Definitions + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and + "OPTIONAL" in this document are to be interpreted as described in BCP + 14 [RFC2119] [RFC8174] when, and only when, they appear in all + +3. SCRAM-SHA-512 and SCRAM-SHA-512-PLUS + + The SCRAM-SHA-512 and SCRAM-SHA-512-PLUS SASL mechanisms are defined + in the same way that SCRAM-SHA-1 and SCRAM-SHA-1-PLUS are defined in + [RFC5802], except that the hash function for HMAC() and H() uses + SHA-512 instead of SHA-1 [RFC6234]. + + For the SCRAM-SHA-512 and SCRAM-SHA-512-PLUS SASL mechanisms, the + hash iteration-count announced by a server SHOULD be at least 10000. + + The GSS-API mechanism OID for SCRAM-SHA-512 is 1.3.6.1.5.5.<TBD> (see + Section 5). + + This is a simple example of a SCRAM-SHA-512 authentication exchange + when the client doesn't support channel bindings. The username + 'user' and password 'pencil' are being used. + + [[TBD: Add an example]] + + + + + +Melnikov Expires May 22, 2021 [Page 2] + +Internet-Draft SASL SCRAM-SHA-512/SCRAM-SHA-512-PLUS November 2020 + + +4. Security Considerations + + The security considerations from [RFC5802] still apply. + + To be secure, either SCRAM-SHA-512-PLUS and SCRAM-SHA-1-PLUS MUST be + used over a TLS channel that has had the session hash extension + [RFC7627] negotiated, or session resumption MUST NOT have been used. + When using SCRAM over TLS 1.2 [RFC5246], the "tls-unique" channel + binding is still the default channel binding to use (see Section 6.1 + of [RFC5802]), assuming the above conditions are satisfied. As "tls- + unique" channel binding is not defined for TLS 1.3 [RFC8446], when + using SCRAM over TLS 1.3, the "tls-exporter" channel binding + [tls-1.3-channel-binding] MUST be the default channel binding (in the + sense specified in Section 6.1 of [RFC5802]) to use. + + See [RFC4270] and [RFC6194] for reasons to move from SHA-1 to a + strong security mechanism like SHA-512. + + The strength of this mechanism is dependent in part on the hash + iteration-count, as denoted by "i" in [RFC5802]. As a rule of thumb, + the hash iteration-count should be such that a modern machine will + take 0.1 seconds to perform the complete algorithm; however, this is + unlikely to be practical on mobile devices and other relatively low- + performance systems. At the time this was written, the rule of thumb + gives around 15,000 iterations required; however, a hash iteration- + count of 10000 takes around 0.5 seconds on current mobile handsets. + This computational cost can be avoided by caching the ClientKey + (assuming the Salt and hash iteration-count is stable). Therefore, + the recommendation of this specification is that the hash iteration- + count SHOULD be at least 10000, but careful consideration ought to be + given to using a significantly higher value, particularly where + mobile use is less important. + +5. IANA Considerations + + IANA is requested to add the following new SASL SCRAM mechanisms to + the "SASL SCRAM Family Mechanisms" registry: + + + + To: iana@iana.org + + Subject: Registration of a new SASL SCRAM Family mechanism SCRAM- + SHA-512 + + SASL mechanism name (or prefix for the family): SCRAM-SHA-512 + + Security considerations: Section 4 of RFC XXXX + + + +Melnikov Expires May 22, 2021 [Page 3] + +Internet-Draft SASL SCRAM-SHA-512/SCRAM-SHA-512-PLUS November 2020 + + + Published specification (optional, recommended): RFC XXXX + + Minimum iteration-count: 10000 + + OID: 1.3.6.1.5.5.<TBD> + + Person & email address to contact for further information: IETF + KITTEN WG <kitten@ietf.org> + + Intended usage: COMMON + + Owner/Change controller: IESG <iesg@ietf.org> + + Note: + + To: iana@iana.org + + Subject: Registration of a new SASL SCRAM Family mechanism SCRAM- + SHA-512-PLUS + + SASL mechanism name (or prefix for the family): SCRAM-SHA- + 512-PLUS + + Security considerations: Section 4 of RFC XXXX + + Published specification (optional, recommended): RFC XXXX + + Minimum iteration-count: 10000 + + OID: 1.3.6.1.5.5.<TBD> + + Person & email address to contact for further information: IETF + KITTEN WG <kitten@ietf.org> + + Intended usage: COMMON + + Owner/Change controller: IESG <iesg@ietf.org> + + Note: + +6. References + +6.1. Normative References + + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, + DOI 10.17487/RFC2119, March 1997, + <https://www.rfc-editor.org/info/rfc2119>. + + + +Melnikov Expires May 22, 2021 [Page 4] + +Internet-Draft SASL SCRAM-SHA-512/SCRAM-SHA-512-PLUS November 2020 + + + [RFC4422] Melnikov, A., Ed. and K. Zeilenga, Ed., "Simple + Authentication and Security Layer (SASL)", RFC 4422, + DOI 10.17487/RFC4422, June 2006, + <https://www.rfc-editor.org/info/rfc4422>. + + [RFC5246] Dierks, T. and E. Rescorla, "The Transport Layer Security + (TLS) Protocol Version 1.2", RFC 5246, + DOI 10.17487/RFC5246, August 2008, + <https://www.rfc-editor.org/info/rfc5246>. + + [RFC5802] Newman, C., Menon-Sen, A., Melnikov, A., and N. Williams, + "Salted Challenge Response Authentication Mechanism + (SCRAM) SASL and GSS-API Mechanisms", RFC 5802, + DOI 10.17487/RFC5802, July 2010, + <https://www.rfc-editor.org/info/rfc5802>. + + [RFC6234] Eastlake 3rd, D. and T. Hansen, "US Secure Hash Algorithms + (SHA and SHA-based HMAC and HKDF)", RFC 6234, + DOI 10.17487/RFC6234, May 2011, + <https://www.rfc-editor.org/info/rfc6234>. + + [RFC7627] Bhargavan, K., Ed., Delignat-Lavaud, A., Pironti, A., + Langley, A., and M. Ray, "Transport Layer Security (TLS) + Session Hash and Extended Master Secret Extension", + RFC 7627, DOI 10.17487/RFC7627, September 2015, + <https://www.rfc-editor.org/info/rfc7627>. + + [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC + 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, + May 2017, <https://www.rfc-editor.org/info/rfc8174>. + + [RFC8446] Rescorla, E., "The Transport Layer Security (TLS) Protocol + Version 1.3", RFC 8446, DOI 10.17487/RFC8446, August 2018, + <https://www.rfc-editor.org/info/rfc8446>. + + [tls-1.3-channel-binding] + Whited, S., "Channel Bindings for TLS 1.3", draft-ietf- + kitten-tls-channel-bindings-for-tls13-00 (work in + progress), June 2020. + +6.2. Informative References + + [RFC4270] Hoffman, P. and B. Schneier, "Attacks on Cryptographic + Hashes in Internet Protocols", RFC 4270, + DOI 10.17487/RFC4270, November 2005, + <https://www.rfc-editor.org/info/rfc4270>. + + + + + +Melnikov Expires May 22, 2021 [Page 5] + +Internet-Draft SASL SCRAM-SHA-512/SCRAM-SHA-512-PLUS November 2020 + + + [RFC5226] Narten, T. and H. Alvestrand, "Guidelines for Writing an + IANA Considerations Section in RFCs", RFC 5226, + DOI 10.17487/RFC5226, May 2008, + <https://www.rfc-editor.org/info/rfc5226>. + + [RFC6194] Polk, T., Chen, L., Turner, S., and P. Hoffman, "Security + Considerations for the SHA-0 and SHA-1 Message-Digest + Algorithms", RFC 6194, DOI 10.17487/RFC6194, March 2011, + <https://www.rfc-editor.org/info/rfc6194>. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Melnikov Expires May 22, 2021 [Page 6] + +Internet-Draft SASL SCRAM-SHA-512/SCRAM-SHA-512-PLUS November 2020 + + +Acknowledgements + + This document is based on RFC 7677 by Tony Hansen. + +Author's Address + + Alexey Melnikov (editor) + Isode Ltd + 14 Castle Mews + Hampton, Middlesex TW12 2NP + UK + + EMail: alexey.melnikov@isode.com + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Melnikov Expires May 22, 2021 [Page 7] diff --git a/src/common/libManageSieve/doc/SASL SCRAM/rfc5802.txt b/src/common/libManageSieve/doc/SASL SCRAM/rfc5802.txt new file mode 100644 index 00000000..971003a7 --- /dev/null +++ b/src/common/libManageSieve/doc/SASL SCRAM/rfc5802.txt @@ -0,0 +1,1571 @@ + + + + + + +Internet Engineering Task Force (IETF) C. Newman +Request for Comments: 5802 Oracle +Category: Standards Track A. Menon-Sen +ISSN: 2070-1721 Oryx Mail Systems GmbH + A. Melnikov + Isode, Ltd. + N. Williams + Oracle + July 2010 + + + Salted Challenge Response Authentication Mechanism (SCRAM) + SASL and GSS-API Mechanisms + +Abstract + + The secure authentication mechanism most widely deployed and used by + Internet application protocols is the transmission of clear-text + passwords over a channel protected by Transport Layer Security (TLS). + There are some significant security concerns with that mechanism, + which could be addressed by the use of a challenge response + authentication mechanism protected by TLS. Unfortunately, the + challenge response mechanisms presently on the standards track all + fail to meet requirements necessary for widespread deployment, and + have had success only in limited use. + + This specification describes a family of Simple Authentication and + Security Layer (SASL; RFC 4422) authentication mechanisms called the + Salted Challenge Response Authentication Mechanism (SCRAM), which + addresses the security concerns and meets the deployability + requirements. When used in combination with TLS or an equivalent + security layer, a mechanism from this family could improve the status + quo for application protocol authentication and provide a suitable + choice for a mandatory-to-implement mechanism for future application + protocol standards. + + + + + + + + + + + + + + + + +Newman, et al. Standards Track [Page 1] + +RFC 5802 SCRAM July 2010 + + +Status of This Memo + + This is an Internet Standards Track document. + + This document is a product of the Internet Engineering Task Force + (IETF). It represents the consensus of the IETF community. It has + received public review and has been approved for publication by the + Internet Engineering Steering Group (IESG). Further information on + Internet Standards is available in Section 2 of RFC 5741. + + Information about the current status of this document, any errata, + and how to provide feedback on it may be obtained at + http://www.rfc-editor.org/info/rfc5802. + +Copyright Notice + + Copyright (c) 2010 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents + (http://trustee.ietf.org/license-info) in effect on the date of + publication of this document. Please review these documents + carefully, as they describe your rights and restrictions with respect + to this document. Code Components extracted from this document must + include Simplified BSD License text as described in Section 4.e of + the Trust Legal Provisions and are provided without warranty as + described in the Simplified BSD License. + + + + + + + + + + + + + + + + + + + + + + + +Newman, et al. Standards Track [Page 2] + +RFC 5802 SCRAM July 2010 + + +Table of Contents + + 1. Introduction ....................................................4 + 2. Conventions Used in This Document ...............................5 + 2.1. Terminology ................................................5 + 2.2. Notation ...................................................6 + 3. SCRAM Algorithm Overview ........................................7 + 4. SCRAM Mechanism Names ...........................................8 + 5. SCRAM Authentication Exchange ...................................9 + 5.1. SCRAM Attributes ..........................................10 + 5.2. Compliance with SASL Mechanism Requirements ...............13 + 6. Channel Binding ................................................14 + 6.1. Default Channel Binding ...................................15 + 7. Formal Syntax ..................................................15 + 8. SCRAM as a GSS-API Mechanism ...................................19 + 8.1. GSS-API Principal Name Types for SCRAM ....................19 + 8.2. GSS-API Per-Message Tokens for SCRAM ......................20 + 8.3. GSS_Pseudo_random() for SCRAM .............................20 + 9. Security Considerations ........................................20 + 10. IANA Considerations ...........................................22 + 11. Acknowledgements ..............................................23 + 12. References ....................................................24 + 12.1. Normative References .....................................24 + 12.2. Normative References for GSS-API Implementors ............24 + 12.3. Informative References ...................................25 + Appendix A. Other Authentication Mechanisms .......................27 + Appendix B. Design Motivations ....................................27 + + + + + + + + + + + + + + + + + + + + + + + + +Newman, et al. Standards Track [Page 3] + +RFC 5802 SCRAM July 2010 + + +1. Introduction + + This specification describes a family of authentication mechanisms + called the Salted Challenge Response Authentication Mechanism (SCRAM) + which addresses the requirements necessary to deploy a challenge- + response mechanism more widely than past attempts (see Appendix A and + Appendix B). When used in combination with Transport Layer Security + (TLS; see [RFC5246]) or an equivalent security layer, a mechanism + from this family could improve the status quo for application + protocol authentication and provide a suitable choice for a + mandatory-to-implement mechanism for future application protocol + standards. + + For simplicity, this family of mechanisms does not presently include + negotiation of a security layer [RFC4422]. It is intended to be used + with an external security layer such as that provided by TLS or SSH, + with optional channel binding [RFC5056] to the external security + layer. + + SCRAM is specified herein as a pure Simple Authentication and + Security Layer (SASL) [RFC4422] mechanism, but it conforms to the new + bridge between SASL and the Generic Security Service Application + Program Interface (GSS-API) called "GS2" [RFC5801]. This means that + this document defines both, a SASL mechanism and a GSS-API mechanism. + + SCRAM provides the following protocol features: + + o The authentication information stored in the authentication + database is not sufficient by itself to impersonate the client. + The information is salted to prevent a pre-stored dictionary + attack if the database is stolen. + + o The server does not gain the ability to impersonate the client to + other servers (with an exception for server-authorized proxies). + + o The mechanism permits the use of a server-authorized proxy without + requiring that proxy to have super-user rights with the back-end + server. + + o Mutual authentication is supported, but only the client is named + (i.e., the server has no name). + + o When used as a SASL mechanism, SCRAM is capable of transporting + authorization identities (see [RFC4422], Section 2) from the + client to the server. + + + + + + +Newman, et al. Standards Track [Page 4] + +RFC 5802 SCRAM July 2010 + + + A separate document defines a standard LDAPv3 [RFC4510] attribute + that enables storage of the SCRAM authentication information in LDAP. + See [RFC5803]. + + For an in-depth discussion of why other challenge response mechanisms + are not considered sufficient, see Appendix A. For more information + about the motivations behind the design of this mechanism, see + Appendix B. + +2. Conventions Used in This Document + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this + document are to be interpreted as described in [RFC2119]. + + Formal syntax is defined by [RFC5234] including the core rules + defined in Appendix B of [RFC5234]. + + Example lines prefaced by "C:" are sent by the client and ones + prefaced by "S:" by the server. If a single "C:" or "S:" label + applies to multiple lines, then the line breaks between those lines + are for editorial clarity only, and are not part of the actual + protocol exchange. + +2.1. Terminology + + This document uses several terms defined in [RFC4949] ("Internet + Security Glossary") including the following: authentication, + authentication exchange, authentication information, brute force, + challenge-response, cryptographic hash function, dictionary attack, + eavesdropping, hash result, keyed hash, man-in-the-middle, nonce, + one-way encryption function, password, replay attack, and salt. + Readers not familiar with these terms should use that glossary as a + reference. + + Some clarifications and additional definitions follow: + + o Authentication information: Information used to verify an identity + claimed by a SCRAM client. The authentication information for a + SCRAM identity consists of salt, iteration count, "StoredKey" and + "ServerKey" (as defined in the algorithm overview) for each + supported cryptographic hash function. + + o Authentication database: The database used to look up the + authentication information associated with a particular identity. + For application protocols, LDAPv3 (see [RFC4510]) is frequently + + + + + +Newman, et al. Standards Track [Page 5] + +RFC 5802 SCRAM July 2010 + + + used as the authentication database. For network-level protocols + such as PPP or 802.11x, the use of RADIUS [RFC2865] is more + common. + + o Base64: An encoding mechanism defined in [RFC4648] that converts + an octet string input to a textual output string that can be + easily displayed to a human. The use of base64 in SCRAM is + restricted to the canonical form with no whitespace. + + o Octet: An 8-bit byte. + + o Octet string: A sequence of 8-bit bytes. + + o Salt: A random octet string that is combined with a password + before applying a one-way encryption function. This value is used + to protect passwords that are stored in an authentication + database. + +2.2. Notation + + The pseudocode description of the algorithm uses the following + notations: + + o ":=": The variable on the left-hand side represents the octet + string resulting from the expression on the right-hand side. + + o "+": Octet string concatenation. + + o "[ ]": A portion of an expression enclosed in "[" and "]" may not + be included in the result under some circumstances. See the + associated text for a description of those circumstances. + + o Normalize(str): Apply the SASLprep profile [RFC4013] of the + "stringprep" algorithm [RFC3454] as the normalization algorithm to + a UTF-8 [RFC3629] encoded "str". The resulting string is also in + UTF-8. When applying SASLprep, "str" is treated as a "stored + strings", which means that unassigned Unicode codepoints are + prohibited (see Section 7 of [RFC3454]). Note that + implementations MUST either implement SASLprep or disallow use of + non US-ASCII Unicode codepoints in "str". + + o HMAC(key, str): Apply the HMAC keyed hash algorithm (defined in + [RFC2104]) using the octet string represented by "key" as the key + and the octet string "str" as the input string. The size of the + result is the hash result size for the hash function in use. For + example, it is 20 octets for SHA-1 (see [RFC3174]). + + + + + +Newman, et al. Standards Track [Page 6] + +RFC 5802 SCRAM July 2010 + + + o H(str): Apply the cryptographic hash function to the octet string + "str", producing an octet string as a result. The size of the + result depends on the hash result size for the hash function in + use. + + o XOR: Apply the exclusive-or operation to combine the octet string + on the left of this operator with the octet string on the right of + this operator. The length of the output and each of the two + inputs will be the same for this use. + + o Hi(str, salt, i): + + U1 := HMAC(str, salt + INT(1)) + U2 := HMAC(str, U1) + ... + Ui-1 := HMAC(str, Ui-2) + Ui := HMAC(str, Ui-1) + + Hi := U1 XOR U2 XOR ... XOR Ui + + where "i" is the iteration count, "+" is the string concatenation + operator, and INT(g) is a 4-octet encoding of the integer g, most + significant octet first. + + Hi() is, essentially, PBKDF2 [RFC2898] with HMAC() as the + pseudorandom function (PRF) and with dkLen == output length of + HMAC() == output length of H(). + +3. SCRAM Algorithm Overview + + The following is a description of a full, uncompressed SASL SCRAM + authentication exchange. Nothing in SCRAM prevents either sending + the client-first message with the SASL authentication request defined + by an application protocol ("initial client response"), or sending + the server-final message as additional data of the SASL outcome of + authentication exchange defined by an application protocol. See + [RFC4422] for more details. + + Note that this section omits some details, such as client and server + nonces. See Section 5 for more details. + + To begin with, the SCRAM client is in possession of a username and + password (*) (or a ClientKey/ServerKey, or SaltedPassword). It sends + the username to the server, which retrieves the corresponding + authentication information, i.e., a salt, StoredKey, ServerKey, and + the iteration count i. (Note that a server implementation may choose + + + + + +Newman, et al. Standards Track [Page 7] + +RFC 5802 SCRAM July 2010 + + + to use the same iteration count for all accounts.) The server sends + the salt and the iteration count to the client, which then computes + the following values and sends a ClientProof to the server: + + (*) Note that both the username and the password MUST be encoded in + UTF-8 [RFC3629]. + + Informative Note: Implementors are encouraged to create test cases + that use both usernames and passwords with non-ASCII codepoints. In + particular, it's useful to test codepoints whose "Unicode + Normalization Form C" and "Unicode Normalization Form KC" are + different. Some examples of such codepoints include Vulgar Fraction + One Half (U+00BD) and Acute Accent (U+00B4). + + SaltedPassword := Hi(Normalize(password), salt, i) + ClientKey := HMAC(SaltedPassword, "Client Key") + StoredKey := H(ClientKey) + AuthMessage := client-first-message-bare + "," + + server-first-message + "," + + client-final-message-without-proof + ClientSignature := HMAC(StoredKey, AuthMessage) + ClientProof := ClientKey XOR ClientSignature + ServerKey := HMAC(SaltedPassword, "Server Key") + ServerSignature := HMAC(ServerKey, AuthMessage) + + The server authenticates the client by computing the ClientSignature, + exclusive-ORing that with the ClientProof to recover the ClientKey + and verifying the correctness of the ClientKey by applying the hash + function and comparing the result to the StoredKey. If the ClientKey + is correct, this proves that the client has access to the user's + password. + + Similarly, the client authenticates the server by computing the + ServerSignature and comparing it to the value sent by the server. If + the two are equal, it proves that the server had access to the user's + ServerKey. + + The AuthMessage is computed by concatenating messages from the + authentication exchange. The format of these messages is defined in + Section 7. + +4. SCRAM Mechanism Names + + A SCRAM mechanism name is a string "SCRAM-" followed by the + uppercased name of the underlying hash function taken from the IANA + "Hash Function Textual Names" registry (see http://www.iana.org), + optionally followed by the suffix "-PLUS" (see below). Note that + SASL mechanism names are limited to 20 octets, which means that only + + + +Newman, et al. Standards Track [Page 8] + +RFC 5802 SCRAM July 2010 + + + hash function names with lengths shorter or equal to 9 octets + (20-length("SCRAM-")-length("-PLUS") can be used. For cases when the + underlying hash function name is longer than 9 octets, an alternative + 9-octet (or shorter) name can be used to construct the corresponding + SCRAM mechanism name, as long as this alternative name doesn't + conflict with any other hash function name from the IANA "Hash + Function Textual Names" registry. In order to prevent future + conflict, such alternative names SHOULD be registered in the IANA + "Hash Function Textual Names" registry. + + For interoperability, all SCRAM clients and servers MUST implement + the SCRAM-SHA-1 authentication mechanism, i.e., an authentication + mechanism from the SCRAM family that uses the SHA-1 hash function as + defined in [RFC3174]. + + The "-PLUS" suffix is used only when the server supports channel + binding to the external channel. If the server supports channel + binding, it will advertise both the "bare" and "plus" versions of + whatever mechanisms it supports (e.g., if the server supports only + SCRAM with SHA-1, then it will advertise support for both SCRAM-SHA-1 + and SCRAM-SHA-1-PLUS). If the server does not support channel + binding, then it will advertise only the "bare" version of the + mechanism (e.g., only SCRAM-SHA-1). The "-PLUS" exists to allow + negotiation of the use of channel binding. See Section 6. + +5. SCRAM Authentication Exchange + + SCRAM is a SASL mechanism whose client response and server challenge + messages are text-based messages containing one or more attribute- + value pairs separated by commas. Each attribute has a one-letter + name. The messages and their attributes are described in + Section 5.1, and defined in Section 7. + + SCRAM is a client-first SASL mechanism (see [RFC4422], Section 5, + item 2a), and returns additional data together with a server's + indication of a successful outcome. + + This is a simple example of a SCRAM-SHA-1 authentication exchange + when the client doesn't support channel bindings (username 'user' and + password 'pencil' are used): + + C: n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL + S: r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92, + i=4096 + C: c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j, + p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts= + S: v=rmF9pqV8S7suAoZWja4dJRkFsKQ= + + + + +Newman, et al. Standards Track [Page 9] + +RFC 5802 SCRAM July 2010 + + + First, the client sends the "client-first-message" containing: + + o a GS2 header consisting of a flag indicating whether channel + binding is supported-but-not-used, not supported, or used, and an + optional SASL authorization identity; + + o SCRAM username and a random, unique nonce attributes. + + Note that the client's first message will always start with "n", "y", + or "p"; otherwise, the message is invalid and authentication MUST + fail. This is important, as it allows for GS2 extensibility (e.g., + to add support for security layers). + + In response, the server sends a "server-first-message" containing the + user's iteration count i and the user's salt, and appends its own + nonce to the client-specified one. + + The client then responds by sending a "client-final-message" with the + same nonce and a ClientProof computed using the selected hash + function as explained earlier. + + The server verifies the nonce and the proof, verifies that the + authorization identity (if supplied by the client in the first + message) is authorized to act as the authentication identity, and, + finally, it responds with a "server-final-message", concluding the + authentication exchange. + + The client then authenticates the server by computing the + ServerSignature and comparing it to the value sent by the server. If + the two are different, the client MUST consider the authentication + exchange to be unsuccessful, and it might have to drop the + connection. + +5.1. SCRAM Attributes + + This section describes the permissible attributes, their use, and the + format of their values. All attribute names are single US-ASCII + letters and are case-sensitive. + + Note that the order of attributes in client or server messages is + fixed, with the exception of extension attributes (described by the + "extensions" ABNF production), which can appear in any order in the + designated positions. See Section 7 for authoritative reference. + + o a: This is an optional attribute, and is part of the GS2 [RFC5801] + bridge between the GSS-API and SASL. This attribute specifies an + authorization identity. A client may include it in its first + message to the server if it wants to authenticate as one user, but + + + +Newman, et al. Standards Track [Page 10] + +RFC 5802 SCRAM July 2010 + + + subsequently act as a different user. This is typically used by + an administrator to perform some management task on behalf of + another user, or by a proxy in some situations. + + Upon the receipt of this value the server verifies its + correctness according to the used SASL protocol profile. + Failed verification results in failed authentication exchange. + + If this attribute is omitted (as it normally would be), the + authorization identity is assumed to be derived from the + username specified with the (required) "n" attribute. + + The server always authenticates the user specified by the "n" + attribute. If the "a" attribute specifies a different user, + the server associates that identity with the connection after + successful authentication and authorization checks. + + The syntax of this field is the same as that of the "n" field + with respect to quoting of '=' and ','. + + o n: This attribute specifies the name of the user whose password is + used for authentication (a.k.a. "authentication identity" + [RFC4422]). A client MUST include it in its first message to the + server. If the "a" attribute is not specified (which would + normally be the case), this username is also the identity that + will be associated with the connection subsequent to + authentication and authorization. + + Before sending the username to the server, the client SHOULD + prepare the username using the "SASLprep" profile [RFC4013] of + the "stringprep" algorithm [RFC3454] treating it as a query + string (i.e., unassigned Unicode code points are allowed). If + the preparation of the username fails or results in an empty + string, the client SHOULD abort the authentication exchange + (*). + + (*) An interactive client can request a repeated entry of the + username value. + + Upon receipt of the username by the server, the server MUST + either prepare it using the "SASLprep" profile [RFC4013] of the + "stringprep" algorithm [RFC3454] treating it as a query string + (i.e., unassigned Unicode codepoints are allowed) or otherwise + be prepared to do SASLprep-aware string comparisons and/or + index lookups. If the preparation of the username fails or + results in an empty string, the server SHOULD abort the + + + + + +Newman, et al. Standards Track [Page 11] + +RFC 5802 SCRAM July 2010 + + + authentication exchange. Whether or not the server prepares + the username using "SASLprep", it MUST use it as received in + hash calculations. + + The characters ',' or '=' in usernames are sent as '=2C' and + '=3D' respectively. If the server receives a username that + contains '=' not followed by either '2C' or '3D', then the + server MUST fail the authentication. + + o m: This attribute is reserved for future extensibility. In this + version of SCRAM, its presence in a client or a server message + MUST cause authentication failure when the attribute is parsed by + the other end. + + o r: This attribute specifies a sequence of random printable ASCII + characters excluding ',' (which forms the nonce used as input to + the hash function). No quoting is applied to this string. As + described earlier, the client supplies an initial value in its + first message, and the server augments that value with its own + nonce in its first response. It is important that this value be + different for each authentication (see [RFC4086] for more details + on how to achieve this). The client MUST verify that the initial + part of the nonce used in subsequent messages is the same as the + nonce it initially specified. The server MUST verify that the + nonce sent by the client in the second message is the same as the + one sent by the server in its first message. + + o c: This REQUIRED attribute specifies the base64-encoded GS2 header + and channel binding data. It is sent by the client in its second + authentication message. The attribute data consist of: + + * the GS2 header from the client's first message (recall that the + GS2 header contains a channel binding flag and an optional + authzid). This header is going to include channel binding type + prefix (see [RFC5056]), if and only if the client is using + channel binding; + + * followed by the external channel's channel binding data, if and + only if the client is using channel binding. + + o s: This attribute specifies the base64-encoded salt used by the + server for this user. It is sent by the server in its first + message to the client. + + o i: This attribute specifies an iteration count for the selected + hash function and user, and MUST be sent by the server along with + the user's salt. + + + + +Newman, et al. Standards Track [Page 12] + +RFC 5802 SCRAM July 2010 + + + For the SCRAM-SHA-1/SCRAM-SHA-1-PLUS SASL mechanism, servers + SHOULD announce a hash iteration-count of at least 4096. Note + that a client implementation MAY cache ClientKey&ServerKey (or + just SaltedPassword) for later reauthentication to the same + service, as it is likely that the server is going to advertise + the same salt value upon reauthentication. This might be + useful for mobile clients where CPU usage is a concern. + + o p: This attribute specifies a base64-encoded ClientProof. The + client computes this value as described in the overview and sends + it to the server. + + o v: This attribute specifies a base64-encoded ServerSignature. It + is sent by the server in its final message, and is used by the + client to verify that the server has access to the user's + authentication information. This value is computed as explained + in the overview. + + o e: This attribute specifies an error that occurred during + authentication exchange. It is sent by the server in its final + message and can help diagnose the reason for the authentication + exchange failure. On failed authentication, the entire server- + final-message is OPTIONAL; specifically, a server implementation + MAY conclude the SASL exchange with a failure without sending the + server-final-message. This results in an application-level error + response without an extra round-trip. If the server-final-message + is sent on authentication failure, then the "e" attribute MUST be + included. + + o As-yet unspecified mandatory and optional extensions. Mandatory + extensions are encoded as values of the 'm' attribute (see ABNF + for reserved-mext in section 7). Optional extensions use as-yet + unassigned attribute names. + + Mandatory extensions sent by one peer but not understood by the + other MUST cause authentication failure (the server SHOULD send + the "extensions-not-supported" server-error-value). + + Unknown optional extensions MUST be ignored upon receipt. + +5.2. Compliance with SASL Mechanism Requirements + + This section describes compliance with SASL mechanism requirements + specified in Section 5 of [RFC4422]. + + 1) "SCRAM-SHA-1" and "SCRAM-SHA-1-PLUS". + + 2a) SCRAM is a client-first mechanism. + + + +Newman, et al. Standards Track [Page 13] + +RFC 5802 SCRAM July 2010 + + + 2b) SCRAM sends additional data with success. + + 3) SCRAM is capable of transferring authorization identities from + the client to the server. + + 4) SCRAM does not offer any security layers (SCRAM offers channel + binding instead). + + 5) SCRAM has a hash protecting the authorization identity. + +6. Channel Binding + + SCRAM supports channel binding to external secure channels, such as + TLS. Clients and servers may or may not support channel binding, + therefore the use of channel binding is negotiable. SCRAM does not + provide security layers, however, therefore it is imperative that + SCRAM provide integrity protection for the negotiation of channel + binding. + + Use of channel binding is negotiated as follows: + + o Servers that support the use of channel binding SHOULD advertise + both the non-PLUS (SCRAM-<hash-function>) and PLUS-variant (SCRAM- + <hash-function>-PLUS) mechanism name. If the server cannot + support channel binding, it SHOULD advertise only the non-PLUS- + variant. If the server would never succeed in the authentication + of the non-PLUS-variant due to policy reasons, it MUST advertise + only the PLUS-variant. + + o If the client supports channel binding and the server does not + appear to (i.e., the client did not see the -PLUS name advertised + by the server), then the client MUST NOT use an "n" gs2-cbind- + flag. + + o Clients that support mechanism negotiation and channel binding + MUST use a "p" gs2-cbind-flag when the server offers the PLUS- + variant of the desired GS2 mechanism. + + o If the client does not support channel binding, then it MUST use + an "n" gs2-cbind-flag. Conversely, if the client requires the use + of channel binding then it MUST use a "p" gs2-cbind-flag. Clients + that do not support mechanism negotiation never use a "y" gs2- + cbind-flag, they use either "p" or "n" according to whether they + require and support the use of channel binding or whether they do + not, respectively. + + o Upon receipt of the client-first message, the server checks the + channel binding flag (gs2-cbind-flag). + + + +Newman, et al. Standards Track [Page 14] + +RFC 5802 SCRAM July 2010 + + + * If the flag is set to "y" and the server supports channel + binding, the server MUST fail authentication. This is because + if the client sets the channel binding flag to "y", then the + client must have believed that the server did not support + channel binding -- if the server did in fact support channel + binding, then this is an indication that there has been a + downgrade attack (e.g., an attacker changed the server's + mechanism list to exclude the -PLUS suffixed SCRAM mechanism + name(s)). + + * If the channel binding flag was "p" and the server does not + support the indicated channel binding type, then the server + MUST fail authentication. + + The server MUST always validate the client's "c=" field. The server + does this by constructing the value of the "c=" attribute and then + checking that it matches the client's c= attribute value. + + For more discussions of channel bindings, and the syntax of channel + binding data for various security protocols, see [RFC5056]. + +6.1. Default Channel Binding + + A default channel binding type agreement process for all SASL + application protocols that do not provide their own channel binding + type agreement is provided as follows. + + 'tls-unique' is the default channel binding type for any application + that doesn't specify one. + + Servers MUST implement the "tls-unique" [RFC5929] channel binding + type, if they implement any channel binding. Clients SHOULD + implement the "tls-unique" [RFC5929] channel binding type, if they + implement any channel binding. Clients and servers SHOULD choose the + highest-layer/innermost end-to-end TLS channel as the channel to + which to bind. + + Servers MUST choose the channel binding type indicated by the client, + or fail authentication if they don't support it. + +7. Formal Syntax + + The following syntax specification uses the Augmented Backus-Naur + form (ABNF) notation as specified in [RFC5234]. "UTF8-2", "UTF8-3", + and "UTF8-4" non-terminal are defined in [RFC3629]. + + + + + + +Newman, et al. Standards Track [Page 15] + +RFC 5802 SCRAM July 2010 + + + ALPHA = <as defined in RFC 5234 appendix B.1> + DIGIT = <as defined in RFC 5234 appendix B.1> + UTF8-2 = <as defined in RFC 3629 (STD 63)> + UTF8-3 = <as defined in RFC 3629 (STD 63)> + UTF8-4 = <as defined in RFC 3629 (STD 63)> + + attr-val = ALPHA "=" value + ;; Generic syntax of any attribute sent + ;; by server or client + + value = 1*value-char + + value-safe-char = %x01-2B / %x2D-3C / %x3E-7F / + UTF8-2 / UTF8-3 / UTF8-4 + ;; UTF8-char except NUL, "=", and ",". + + value-char = value-safe-char / "=" + + printable = %x21-2B / %x2D-7E + ;; Printable ASCII except ",". + ;; Note that any "printable" is also + ;; a valid "value". + + base64-char = ALPHA / DIGIT / "/" / "+" + + base64-4 = 4base64-char + + base64-3 = 3base64-char "=" + + base64-2 = 2base64-char "==" + + base64 = *base64-4 [base64-3 / base64-2] + + posit-number = %x31-39 *DIGIT + ;; A positive number. + + saslname = 1*(value-safe-char / "=2C" / "=3D") + ;; Conforms to <value>. + + authzid = "a=" saslname + ;; Protocol specific. + + cb-name = 1*(ALPHA / DIGIT / "." / "-") + ;; See RFC 5056, Section 7. + ;; E.g., "tls-server-end-point" or + ;; "tls-unique". + + + + + +Newman, et al. Standards Track [Page 16] + +RFC 5802 SCRAM July 2010 + + + gs2-cbind-flag = ("p=" cb-name) / "n" / "y" + ;; "n" -> client doesn't support channel binding. + ;; "y" -> client does support channel binding + ;; but thinks the server does not. + ;; "p" -> client requires channel binding. + ;; The selected channel binding follows "p=". + + gs2-header = gs2-cbind-flag "," [ authzid ] "," + ;; GS2 header for SCRAM + ;; (the actual GS2 header includes an optional + ;; flag to indicate that the GSS mechanism is not + ;; "standard", but since SCRAM is "standard", we + ;; don't include that flag). + + username = "n=" saslname + ;; Usernames are prepared using SASLprep. + + reserved-mext = "m=" 1*(value-char) + ;; Reserved for signaling mandatory extensions. + ;; The exact syntax will be defined in + ;; the future. + + channel-binding = "c=" base64 + ;; base64 encoding of cbind-input. + + proof = "p=" base64 + + nonce = "r=" c-nonce [s-nonce] + ;; Second part provided by server. + + c-nonce = printable + + s-nonce = printable + + salt = "s=" base64 + + verifier = "v=" base64 + ;; base-64 encoded ServerSignature. + + iteration-count = "i=" posit-number + ;; A positive number. + + client-first-message-bare = + [reserved-mext ","] + username "," nonce ["," extensions] + + client-first-message = + gs2-header client-first-message-bare + + + +Newman, et al. Standards Track [Page 17] + +RFC 5802 SCRAM July 2010 + + + server-first-message = + [reserved-mext ","] nonce "," salt "," + iteration-count ["," extensions] + + client-final-message-without-proof = + channel-binding "," nonce ["," + extensions] + + client-final-message = + client-final-message-without-proof "," proof + + server-error = "e=" server-error-value + + server-error-value = "invalid-encoding" / + "extensions-not-supported" / ; unrecognized 'm' value + "invalid-proof" / + "channel-bindings-dont-match" / + "server-does-support-channel-binding" / + ; server does not support channel binding + "channel-binding-not-supported" / + "unsupported-channel-binding-type" / + "unknown-user" / + "invalid-username-encoding" / + ; invalid username encoding (invalid UTF-8 or + ; SASLprep failed) + "no-resources" / + "other-error" / + server-error-value-ext + ; Unrecognized errors should be treated as "other-error". + ; In order to prevent information disclosure, the server + ; may substitute the real reason with "other-error". + + server-error-value-ext = value + ; Additional error reasons added by extensions + ; to this document. + + server-final-message = (server-error / verifier) + ["," extensions] + + extensions = attr-val *("," attr-val) + ;; All extensions are optional, + ;; i.e., unrecognized attributes + ;; not defined in this document + ;; MUST be ignored. + + cbind-data = 1*OCTET + + + + + +Newman, et al. Standards Track [Page 18] + +RFC 5802 SCRAM July 2010 + + + cbind-input = gs2-header [ cbind-data ] + ;; cbind-data MUST be present for + ;; gs2-cbind-flag of "p" and MUST be absent + ;; for "y" or "n". + +8. SCRAM as a GSS-API Mechanism + + This section and its sub-sections and all normative references of it + not referenced elsewhere in this document are INFORMATIONAL for SASL + implementors, but they are NORMATIVE for GSS-API implementors. + + SCRAM is actually also a GSS-API mechanism. The messages are the + same, but a) the GS2 header on the client's first message and channel + binding data is excluded when SCRAM is used as a GSS-API mechanism, + and b) the RFC2743 section 3.1 initial context token header is + prefixed to the client's first authentication message (context + token). + + The GSS-API mechanism OID for SCRAM-SHA-1 is 1.3.6.1.5.5.14 (see + Section 10). + + SCRAM security contexts always have the mutual_state flag + (GSS_C_MUTUAL_FLAG) set to TRUE. SCRAM does not support credential + delegation, therefore SCRAM security contexts alway have the + deleg_state flag (GSS_C_DELEG_FLAG) set to FALSE. + +8.1. GSS-API Principal Name Types for SCRAM + + SCRAM does not explicitly name acceptor principals. However, the use + of acceptor principal names to find or prompt for passwords is + useful. Therefore, SCRAM supports standard generic name syntaxes for + acceptors such as GSS_C_NT_HOSTBASED_SERVICE (see [RFC2743], Section + 4.1). Implementations should use the target name passed to + GSS_Init_sec_context(), if any, to help retrieve or prompt for SCRAM + passwords. + + SCRAM supports only a single name type for initiators: + GSS_C_NT_USER_NAME. GSS_C_NT_USER_NAME is the default name type for + SCRAM. + + There is no name canonicalization procedure for SCRAM beyond applying + SASLprep as described in Section 5.1. + + The query, display, and exported name syntaxes for SCRAM principal + names are all the same. There are no SCRAM-specific name syntaxes + (SCRAM initiator principal names are free-form); -- applications + should use generic GSS-API name types such as GSS_C_NT_USER_NAME and + + + + +Newman, et al. Standards Track [Page 19] + +RFC 5802 SCRAM July 2010 + + + GSS_C_NT_HOSTBASED_SERVICE (see [RFC2743], Section 4). The exported + name token does, of course, conform to [RFC2743], Section 3.2, but + the "NAME" part of the token is just a SCRAM user name. + +8.2. GSS-API Per-Message Tokens for SCRAM + + The per-message tokens for SCRAM as a GSS-API mechanism SHALL be the + same as those for the Kerberos V GSS-API mechanism [RFC4121] (see + Section 4.2 and sub-sections), using the Kerberos V "aes128-cts-hmac- + sha1-96" enctype [RFC3962]. + + The replay_det_state (GSS_C_REPLAY_FLAG), sequence_state + (GSS_C_SEQUENCE_FLAG), conf_avail (GSS_C_CONF_FLAG) and integ_avail + (GSS_C_CONF_FLAG) security context flags are always set to TRUE. + + The 128-bit session "protocol key" SHALL be derived by using the + least significant (right-most) 128 bits of HMAC(StoredKey, "GSS-API + session key" || ClientKey || AuthMessage). "Specific keys" are then + derived as usual as described in Section 2 of [RFC4121], [RFC3961], + and [RFC3962]. + + The terms "protocol key" and "specific key" are Kerberos V5 terms + [RFC3961]. + + SCRAM does support PROT_READY, and is PROT_READY on the initiator + side first upon receipt of the server's reply to the initial security + context token. + +8.3. GSS_Pseudo_random() for SCRAM + + The GSS_Pseudo_random() [RFC4401] for SCRAM SHALL be the same as for + the Kerberos V GSS-API mechanism [RFC4402]. There is no acceptor- + asserted sub-session key for SCRAM, thus GSS_C_PRF_KEY_FULL and + GSS_C_PRF_KEY_PARTIAL are equivalent for SCRAM's GSS_Pseudo_random(). + The protocol key to be used for the GSS_Pseudo_random() SHALL be the + same as the key defined in Section 8.2. + +9. Security Considerations + + If the authentication exchange is performed without a strong security + layer (such as TLS with data confidentiality), then a passive + eavesdropper can gain sufficient information to mount an offline + dictionary or brute-force attack that can be used to recover the + user's password. The amount of time necessary for this attack + depends on the cryptographic hash function selected, the strength of + the password, and the iteration count supplied by the server. An + external security layer with strong encryption will prevent this + attack. + + + +Newman, et al. Standards Track [Page 20] + +RFC 5802 SCRAM July 2010 + + + If the external security layer used to protect the SCRAM exchange + uses an anonymous key exchange, then the SCRAM channel binding + mechanism can be used to detect a man-in-the-middle attack on the + security layer and cause the authentication to fail as a result. + However, the man-in-the-middle attacker will have gained sufficient + information to mount an offline dictionary or brute-force attack. + For this reason, SCRAM allows to increase the iteration count over + time. (Note that a server that is only in possession of "StoredKey" + and "ServerKey" can't automatically increase the iteration count upon + successful authentication. Such an increase would require resetting + the user's password.) + + If the authentication information is stolen from the authentication + database, then an offline dictionary or brute-force attack can be + used to recover the user's password. The use of salt mitigates this + attack somewhat by requiring a separate attack on each password. + Authentication mechanisms that protect against this attack are + available (e.g., the EKE class of mechanisms). RFC 2945 [RFC2945] is + an example of such technology. The WG elected not to use EKE like + mechanisms as a basis for SCRAM. + + If an attacker obtains the authentication information from the + authentication repository and either eavesdrops on one authentication + exchange or impersonates a server, the attacker gains the ability to + impersonate that user to all servers providing SCRAM access using the + same hash function, password, iteration count, and salt. For this + reason, it is important to use randomly generated salt values. + + SCRAM does not negotiate a hash function to use. Hash function + negotiation is left to the SASL mechanism negotiation. It is + important that clients be able to sort a locally available list of + mechanisms by preference so that the client may pick the appropriate + mechanism to use from a server's advertised mechanism list. This + preference order is not specified here as it is a local matter. The + preference order should include objective and subjective notions of + mechanism cryptographic strength (e.g., SCRAM with a successor to + SHA-1 may be preferred over SCRAM with SHA-1). + + Note that to protect the SASL mechanism negotiation applications + normally must list the server mechanisms twice: once before and once + after authentication, the latter using security layers. Since SCRAM + does not provide security layers, the only ways to protect the + mechanism negotiation are a) use channel binding to an external + channel, or b) use an external channel that authenticates a user- + provided server name. + + + + + + +Newman, et al. Standards Track [Page 21] + +RFC 5802 SCRAM July 2010 + + + SCRAM does not protect against downgrade attacks of channel binding + types. The complexities of negotiating a channel binding type, and + handling down-grade attacks in that negotiation, were intentionally + left out of scope for this document. + + A hostile server can perform a computational denial-of-service attack + on clients by sending a big iteration count value. + + See [RFC4086] for more information about generating randomness. + +10. IANA Considerations + + IANA has added the following family of SASL mechanisms to the SASL + Mechanism registry established by [RFC4422]: + + To: iana@iana.org + Subject: Registration of a new SASL family SCRAM + + SASL mechanism name (or prefix for the family): SCRAM-* + Security considerations: Section 7 of [RFC5802] + Published specification (optional, recommended): [RFC5802] + Person & email address to contact for further information: + IETF SASL WG <sasl@ietf.org> + Intended usage: COMMON + Owner/Change controller: IESG <iesg@ietf.org> + Note: Members of this family MUST be explicitly registered + using the "IETF Review" [RFC5226] registration procedure. + Reviews MUST be requested on the SASL mailing list + <sasl@ietf.org> (or a successor designated by the responsible + Security AD). + + Note to future SCRAM-mechanism designers: each new SCRAM-SASL + mechanism MUST be explicitly registered with IANA and MUST comply + with SCRAM-mechanism naming convention defined in Section 4 of this + document. + + + + + + + + + + + + + + + + +Newman, et al. Standards Track [Page 22] + +RFC 5802 SCRAM July 2010 + + + IANA has added the following entries to the SASL Mechanism registry + established by [RFC4422]: + + To: iana@iana.org + Subject: Registration of a new SASL mechanism SCRAM-SHA-1 + + SASL mechanism name (or prefix for the family): SCRAM-SHA-1 + Security considerations: Section 7 of [RFC5802] + Published specification (optional, recommended): [RFC5802] + Person & email address to contact for further information: + IETF SASL WG <sasl@ietf.org> + Intended usage: COMMON + Owner/Change controller: IESG <iesg@ietf.org> + Note: + + To: iana@iana.org + Subject: Registration of a new SASL mechanism SCRAM-SHA-1-PLUS + + SASL mechanism name (or prefix for the family): SCRAM-SHA-1-PLUS + Security considerations: Section 7 of [RFC5802] + Published specification (optional, recommended): [RFC5802] + Person & email address to contact for further information: + IETF SASL WG <sasl@ietf.org> + Intended usage: COMMON + Owner/Change controller: IESG <iesg@ietf.org> + Note: + + Per this document, IANA has assigned a GSS-API mechanism OID for + SCRAM-SHA-1 from the iso.org.dod.internet.security.mechanisms prefix + (see "SMI Security for Mechanism Codes" registry). + +11. Acknowledgements + + This document benefited from discussions on the SASL WG mailing list. + The authors would like to specially thank Dave Cridland, Simon + Josefsson, Jeffrey Hutzelman, Kurt Zeilenga, Pasi Eronen, Ben + Campbell, Peter Saint-Andre, and Tobias Markmann for their + contributions to this document. A special thank you to Simon + Josefsson for shepherding this document and for doing one of the + first implementations of this specification. + + + + + + + + + + + +Newman, et al. Standards Track [Page 23] + +RFC 5802 SCRAM July 2010 + + +12. References + +12.1. Normative References + + [RFC2104] Krawczyk, H., Bellare, M., and R. Canetti, "HMAC: Keyed- + Hashing for Message Authentication", RFC 2104, + February 1997. + + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, March 1997. + + [RFC3174] Eastlake, D. and P. Jones, "US Secure Hash Algorithm 1 + (SHA1)", RFC 3174, September 2001. + + [RFC3454] Hoffman, P. and M. Blanchet, "Preparation of + Internationalized Strings ("stringprep")", RFC 3454, + December 2002. + + [RFC3629] Yergeau, F., "UTF-8, a transformation format of ISO + 10646", STD 63, RFC 3629, November 2003. + + [RFC4013] Zeilenga, K., "SASLprep: Stringprep Profile for User Names + and Passwords", RFC 4013, February 2005. + + [RFC4422] Melnikov, A. and K. Zeilenga, "Simple Authentication and + Security Layer (SASL)", RFC 4422, June 2006. + + [RFC4648] Josefsson, S., "The Base16, Base32, and Base64 Data + Encodings", RFC 4648, October 2006. + + [RFC5056] Williams, N., "On the Use of Channel Bindings to Secure + Channels", RFC 5056, November 2007. + + [RFC5234] Crocker, D. and P. Overell, "Augmented BNF for Syntax + Specifications: ABNF", STD 68, RFC 5234, January 2008. + + [RFC5929] Altman, J., Williams, N., and L. Zhu, "Channel Bindings + for TLS", RFC 5929, July 2010. + +12.2. Normative References for GSS-API Implementors + + [RFC2743] Linn, J., "Generic Security Service Application Program + Interface Version 2, Update 1", RFC 2743, January 2000. + + [RFC3961] Raeburn, K., "Encryption and Checksum Specifications for + Kerberos 5", RFC 3961, February 2005. + + + + + +Newman, et al. Standards Track [Page 24] + +RFC 5802 SCRAM July 2010 + + + [RFC3962] Raeburn, K., "Advanced Encryption Standard (AES) + Encryption for Kerberos 5", RFC 3962, February 2005. + + [RFC4121] Zhu, L., Jaganathan, K., and S. Hartman, "The Kerberos + Version 5 Generic Security Service Application Program + Interface (GSS-API) Mechanism: Version 2", RFC 4121, + July 2005. + + [RFC4401] Williams, N., "A Pseudo-Random Function (PRF) API + Extension for the Generic Security Service Application + Program Interface (GSS-API)", RFC 4401, February 2006. + + [RFC4402] Williams, N., "A Pseudo-Random Function (PRF) for the + Kerberos V Generic Security Service Application Program + Interface (GSS-API) Mechanism", RFC 4402, February 2006. + + [RFC5801] Josefsson, S. and N. Williams, "Using Generic Security + Service Application Program Interface (GSS-API) Mechanisms + in Simple Authentication and Security Layer (SASL): The + GS2 Mechanism Family", RFC 5801, July 2010. + +12.3. Informative References + + [CRAMHISTORIC] + Zeilenga, K., "CRAM-MD5 to Historic", Work in Progress, + November 2008. + + [DIGESTHISTORIC] + Melnikov, A., "Moving DIGEST-MD5 to Historic", Work + in Progress, July 2008. + + [RFC2865] Rigney, C., Willens, S., Rubens, A., and W. Simpson, + "Remote Authentication Dial In User Service (RADIUS)", + RFC 2865, June 2000. + + [RFC2898] Kaliski, B., "PKCS #5: Password-Based Cryptography + Specification Version 2.0", RFC 2898, September 2000. + + [RFC2945] Wu, T., "The SRP Authentication and Key Exchange System", + RFC 2945, September 2000. + + [RFC4086] Eastlake, D., Schiller, J., and S. Crocker, "Randomness + Requirements for Security", BCP 106, RFC 4086, June 2005. + + [RFC4510] Zeilenga, K., "Lightweight Directory Access Protocol + (LDAP): Technical Specification Road Map", RFC 4510, + June 2006. + + + + +Newman, et al. Standards Track [Page 25] + +RFC 5802 SCRAM July 2010 + + + [RFC4616] Zeilenga, K., "The PLAIN Simple Authentication and + Security Layer (SASL) Mechanism", RFC 4616, August 2006. + + [RFC4949] Shirey, R., "Internet Security Glossary, Version 2", + RFC 4949, August 2007. + + [RFC5226] Narten, T. and H. Alvestrand, "Guidelines for Writing an + IANA Considerations Section in RFCs", BCP 26, RFC 5226, + May 2008. + + [RFC5246] Dierks, T. and E. Rescorla, "The Transport Layer Security + (TLS) Protocol Version 1.2", RFC 5246, August 2008. + + [RFC5803] Melnikov, A., "Lightweight Directory Access Protocol + (LDAP) Schema for Storing Salted Challenge Response + Authentication Mechanism (SCRAM) Secrets", RFC 5803, + July 2010. + + [tls-server-end-point] + IANA, "Registration of TLS server end-point channel + bindings", available from http://www.iana.org, June 2008. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Newman, et al. Standards Track [Page 26] + +RFC 5802 SCRAM July 2010 + + +Appendix A. Other Authentication Mechanisms + + The DIGEST-MD5 [DIGESTHISTORIC] mechanism has proved to be too + complex to implement and test, and thus has poor interoperability. + The security layer is often not implemented, and almost never used; + everyone uses TLS instead. For a more complete list of problems with + DIGEST-MD5 that led to the creation of SCRAM, see [DIGESTHISTORIC]. + + The CRAM-MD5 SASL mechanism, while widely deployed, also has some + problems. In particular, it is missing some modern SASL features + such as support for internationalized usernames and passwords, + support for passing of authorization identity, and support for + channel bindings. It also doesn't support server authentication. + For a more complete list of problems with CRAM-MD5, see + [CRAMHISTORIC]. + + The PLAIN [RFC4616] SASL mechanism allows a malicious server or + eavesdropper to impersonate the authenticating user to any other + server for which the user has the same password. It also sends the + password in the clear over the network, unless TLS is used. Server + authentication is not supported. + +Appendix B. Design Motivations + + The following design goals shaped this document. Note that some of + the goals have changed since the initial version of the document. + + o The SASL mechanism has all modern SASL features: support for + internationalized usernames and passwords, support for passing of + authorization identity, and support for channel bindings. + + o The protocol supports mutual authentication. + + o The authentication information stored in the authentication + database is not sufficient by itself to impersonate the client. + + o The server does not gain the ability to impersonate the client to + other servers (with an exception for server-authorized proxies), + unless such other servers allow SCRAM authentication and use the + same salt and iteration count for the user. + + o The mechanism is extensible, but (hopefully) not over-engineered + in this respect. + + o The mechanism is easier to implement than DIGEST-MD5 in both + clients and servers. + + + + + +Newman, et al. Standards Track [Page 27] + +RFC 5802 SCRAM July 2010 + + +Authors' Addresses + + Chris Newman + Oracle + 800 Royal Oaks + Monrovia, CA 91016 + USA + + EMail: chris.newman@oracle.com + + + Abhijit Menon-Sen + Oryx Mail Systems GmbH + + EMail: ams@toroid.org + + + Alexey Melnikov + Isode, Ltd. + + EMail: Alexey.Melnikov@isode.com + + + Nicolas Williams + Oracle + 5300 Riata Trace Ct + Austin, TX 78727 + USA + + EMail: Nicolas.Williams@oracle.com + + + + + + + + + + + + + + + + + + + + + +Newman, et al. Standards Track [Page 28] + diff --git a/src/common/libManageSieve/doc/SASL SCRAM/rfc7677.txt b/src/common/libManageSieve/doc/SASL SCRAM/rfc7677.txt new file mode 100644 index 00000000..d90ea868 --- /dev/null +++ b/src/common/libManageSieve/doc/SASL SCRAM/rfc7677.txt @@ -0,0 +1,451 @@ + + + + + + +Internet Engineering Task Force (IETF) T. Hansen +Request for Comments: 7677 AT&T Laboratories +Updates: 5802 November 2015 +Category: Standards Track +ISSN: 2070-1721 + + + SCRAM-SHA-256 and SCRAM-SHA-256-PLUS + Simple Authentication and Security Layer (SASL) Mechanisms + +Abstract + + This document registers the Simple Authentication and Security Layer + (SASL) mechanisms SCRAM-SHA-256 and SCRAM-SHA-256-PLUS, provides + guidance for secure implementation of the original SCRAM-SHA-1-PLUS + mechanism, and updates the SCRAM registration procedures of RFC 5802. + +Status of This Memo + + This is an Internet Standards Track document. + + This document is a product of the Internet Engineering Task Force + (IETF). It represents the consensus of the IETF community. It has + received public review and has been approved for publication by the + Internet Engineering Steering Group (IESG). Further information on + Internet Standards is available in Section 2 of RFC 5741. + + Information about the current status of this document, any errata, + and how to provide feedback on it may be obtained at + http://www.rfc-editor.org/info/rfc7677. + +Copyright Notice + + Copyright (c) 2015 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents + (http://trustee.ietf.org/license-info) in effect on the date of + publication of this document. Please review these documents + carefully, as they describe your rights and restrictions with respect + to this document. Code Components extracted from this document must + include Simplified BSD License text as described in Section 4.e of + the Trust Legal Provisions and are provided without warranty as + described in the Simplified BSD License. + + + + + + +Hansen Standards Track [Page 1] + +RFC 7677 SASL SCRAM-SHA-256/SCRAM-SHA-256-PLUS November 2015 + + +Table of Contents + + 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 2 + 2. Key Word Definitions . . . . . . . . . . . . . . . . . . . . 2 + 3. SCRAM-SHA-256 and SCRAM-SHA-256-PLUS . . . . . . . . . . . . 2 + 4. Security Considerations . . . . . . . . . . . . . . . . . . . 3 + 5. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 4 + 5.1. Updates to SCRAM-* Registration . . . . . . . . . . . . . 4 + 5.2. SASL-SCRAM Family Mechanisms Registration Procedure . . . 4 + 6. References . . . . . . . . . . . . . . . . . . . . . . . . . 6 + 6.1. Normative References . . . . . . . . . . . . . . . . . . 6 + 6.2. Informative References . . . . . . . . . . . . . . . . . 7 + Acknowledgements . . . . . . . . . . . . . . . . . . . . . . . . 7 + Author's Address . . . . . . . . . . . . . . . . . . . . . . . . 8 + +1. Introduction + + This document registers the SASL mechanisms SCRAM-SHA-256 and SCRAM- + SHA-256-PLUS. SHA-256 has stronger security properties than SHA-1, + and it is expected that SCRAM mechanisms based on it will have + greater predicted longevity than the SCRAM mechanisms based on SHA-1. + + The registration form for the SCRAM family of algorithms is also + updated from [RFC5802]. + + After publication of [RFC5802], it was discovered that Transport + Layer Security (TLS) [RFC5246] does not have the expected properties + for the "tls-unique" channel binding to be secure [RFC7627]. + Therefore, this document contains normative text that applies to both + the original SCRAM-SHA-1-PLUS and the newly introduced SCRAM-SHA- + 256-PLUS mechanism. + +2. Key Word Definitions + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this + document are to be interpreted as described in [RFC2119]. + +3. SCRAM-SHA-256 and SCRAM-SHA-256-PLUS + + The SCRAM-SHA-256 and SCRAM-SHA-256-PLUS SASL mechanisms are defined + in the same way that SCRAM-SHA-1 and SCRAM-SHA-1-PLUS are defined in + [RFC5802], except that the hash function for HMAC() and H() uses + SHA-256 instead of SHA-1 [RFC6234]. + + For the SCRAM-SHA-256 and SCRAM-SHA-256-PLUS SASL mechanisms, the + hash iteration-count announced by a server SHOULD be at least 4096. + + + + +Hansen Standards Track [Page 2] + +RFC 7677 SASL SCRAM-SHA-256/SCRAM-SHA-256-PLUS November 2015 + + + The GSS-API mechanism OID for SCRAM-SHA-256 is 1.3.6.1.5.5.18 (see + Section 5). + + This is a simple example of a SCRAM-SHA-256 authentication exchange + when the client doesn't support channel bindings. The username + 'user' and password 'pencil' are being used. + + C: n,,n=user,r=rOprNGfwEbeRWgbNEkqO + + S: r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0, + s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096 + + C: c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0, + p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ= + + S: v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4= + +4. Security Considerations + + The security considerations from [RFC5802] still apply. + + To be secure, either SCRAM-SHA-256-PLUS and SCRAM-SHA-1-PLUS MUST be + used over a TLS channel that has had the session hash extension + [RFC7627] negotiated, or session resumption MUST NOT have been used. + + See [RFC4270] and [RFC6194] for reasons to move from SHA-1 to a + strong security mechanism like SHA-256. + + The strength of this mechanism is dependent in part on the hash + iteration-count, as denoted by "i" in [RFC5802]. As a rule of thumb, + the hash iteration-count should be such that a modern machine will + take 0.1 seconds to perform the complete algorithm; however, this is + unlikely to be practical on mobile devices and other relatively low- + performance systems. At the time this was written, the rule of thumb + gives around 15,000 iterations required; however, a hash iteration- + count of 4096 takes around 0.5 seconds on current mobile handsets. + This computational cost can be avoided by caching the ClientKey + (assuming the Salt and hash iteration-count is stable). Therefore, + the recommendation of this specification is that the hash iteration- + count SHOULD be at least 4096, but careful consideration ought to be + given to using a significantly higher value, particularly where + mobile use is less important. + + + + + + + + + +Hansen Standards Track [Page 3] + +RFC 7677 SASL SCRAM-SHA-256/SCRAM-SHA-256-PLUS November 2015 + + +5. IANA Considerations + +5.1. Updates to SCRAM-* Registration + + The IANA registry for SCRAM-* (the SCRAM family of SASL mechanisms) + in the SASL mechanism registry ([RFC4422]) has been updated as + follows. The email address for reviews has been updated, and the + note at the end changed. + + To: iana@iana.org + Subject: Registration of a new SASL family SCRAM + + SASL mechanism name (or prefix for the family): SCRAM-* + Security considerations: Section 7 of [RFC5802] + Published specification (optional, recommended): RFC 7677 + Person & email address to contact for further information: + IETF KITTEN WG <kitten@ietf.org> + Intended usage: COMMON + Owner/Change controller: IESG <iesg@ietf.org> + Note: Members of this family MUST be explicitly registered using + the "IETF Review" [RFC5226] registration procedure. Reviews + MUST be requested on the KITTEN mailing list kitten@ietf.org + (or a successor designated by the responsible Security AD). + + Note to future SCRAM-mechanism designers: each new SASL SCRAM + mechanism MUST be explicitly registered with IANA within the SASL + SCRAM Family Mechanisms registry. + +5.2. SASL-SCRAM Family Mechanisms Registration Procedure + + A new IANA registry has been added for members of the SCRAM family of + SASL mechanisms, named "SASL SCRAM Family Mechanisms". It adds two + new fields to the existing SCRAM mechanism registry: Minimum + iteration-count and Associated OID. Below is the template for + registration of a new SASL family SCRAM. (Note that the string + "TBD-BY-IANA" should be left as is, so that it may be filled in at + registration time by IANA.) + + + + + + + + + + + + + + +Hansen Standards Track [Page 4] + +RFC 7677 SASL SCRAM-SHA-256/SCRAM-SHA-256-PLUS November 2015 + + + To: iana@iana.org + Subject: Registration of a new SASL SCRAM family mechanism + + SASL mechanism name (or prefix for the family): SCRAM-<NAME> + Security considerations: Section 7 of [RFC5802] + Published specification (optional, recommended): RFC 7677 + Minimum iteration-count: The minimum hash iteration-count that + servers SHOULD announce + Associated OID: TBD-BY-IANA + Person & email address to contact for further information: + IETF KITTEN WG <kitten@ietf.org> + Intended usage: COMMON + Owner/Change controller: IESG <iesg@ietf.org> + + Note: Members of this family MUST be explicitly registered using + the "IETF Review" [RFC5226] registration procedure. Reviews MUST + be requested on the KITTEN mailing list kitten@ietf.org (or a + successor designated by the responsible Security Area Director). + + Note: At publication of a new SASL SCRAM Family Mechanism, IANA + SHOULD assign a GSS-API mechanism OID for this mechanism from the + iso.org.dod.internet.security.mechanisms prefix (see the "SMI + Security for Mechanism Codes" registry) and fill in the value for + "TBD-BY-IANA" above. Only one OID needs to be assigned for a + SCRAM-<NAME> and SCRAM-<NAME>-PLUS pair. The same OID should be + assigned to both entries in the registry. + + Note to future SASL SCRAM mechanism designers: each new SASL SCRAM + mechanism MUST be explicitly registered with IANA and MUST comply + with the SCRAM-mechanism naming convention defined in Section 4 of + [RFC5802]. + + The existing entries for SASL SCRAM-SHA-1 and SCRAM-SHA-1-PLUS have + been moved from the existing SASL mechanism registry to the "SASL + SCRAM Family Mechanisms" registry. At that time, the following + values were added: + + Minimum iteration-count: 4096 + OID: 1.3.6.1.5.5.14 (from [RFC5802]) + + + + + + + + + + + + +Hansen Standards Track [Page 5] + +RFC 7677 SASL SCRAM-SHA-256/SCRAM-SHA-256-PLUS November 2015 + + + The following new SASL SCRAM mechanisms have been added to the "SASL + SCRAM Family Mechanisms" registry: + + To: iana@iana.org + Subject: Registration of a new SASL SCRAM Family mechanism + SCRAM-SHA-256 + + SASL mechanism name (or prefix for the family): SCRAM-SHA-256 + Security considerations: Section 4 of RFC 7677 + Published specification (optional, recommended): RFC 7677 + Minimum iteration-count: 4096 + OID: 1.3.6.1.5.5.18 + Person & email address to contact for further information: + IETF KITTEN WG <kitten@ietf.org> + Intended usage: COMMON + Owner/Change controller: IESG <iesg@ietf.org> + Note: + + To: iana@iana.org + Subject: Registration of a new SASL SCRAM Family mechanism + SCRAM-SHA-256-PLUS + + SASL mechanism name (or prefix for the family): SCRAM-SHA-256-PLUS + Security considerations: Section 4 of RFC 7677 + Published specification (optional, recommended): RFC 7677 + Minimum iteration-count: 4096 + OID: 1.3.6.1.5.5.18 + Person & email address to contact for further information: + IETF KITTEN WG <kitten@ietf.org> + Intended usage: COMMON + Owner/Change controller: IESG <iesg@ietf.org> + Note: + +6. References + +6.1. Normative References + + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, + DOI 10.17487/RFC2119, March 1997, + <http://www.rfc-editor.org/info/rfc2119>. + + [RFC4422] Melnikov, A., Ed. and K. Zeilenga, Ed., "Simple + Authentication and Security Layer (SASL)", RFC 4422, + DOI 10.17487/RFC4422, June 2006, + <http://www.rfc-editor.org/info/rfc4422>. + + + + + +Hansen Standards Track [Page 6] + +RFC 7677 SASL SCRAM-SHA-256/SCRAM-SHA-256-PLUS November 2015 + + + [RFC5802] Newman, C., Menon-Sen, A., Melnikov, A., and N. Williams, + "Salted Challenge Response Authentication Mechanism + (SCRAM) SASL and GSS-API Mechanisms", RFC 5802, + DOI 10.17487/RFC5802, July 2010, + <http://www.rfc-editor.org/info/rfc5802>. + + [RFC6234] Eastlake 3rd, D. and T. Hansen, "US Secure Hash Algorithms + (SHA and SHA-based HMAC and HKDF)", RFC 6234, + DOI 10.17487/RFC6234, May 2011, + <http://www.rfc-editor.org/info/rfc6234>. + + [RFC7627] Bhargavan, K., Ed., Delignat-Lavaud, A., Pironti, A., + Langley, A., and M. Ray, "Transport Layer Security (TLS) + Session Hash and Extended Master Secret Extension", + RFC 7627, DOI 10.17487/RFC7627, September 2015, + <http://www.rfc-editor.org/info/rfc7627>. + +6.2. Informative References + + [RFC4270] Hoffman, P. and B. Schneier, "Attacks on Cryptographic + Hashes in Internet Protocols", RFC 4270, + DOI 10.17487/RFC4270, November 2005, + <http://www.rfc-editor.org/info/rfc4270>. + + [RFC5226] Narten, T. and H. Alvestrand, "Guidelines for Writing an + IANA Considerations Section in RFCs", BCP 26, RFC 5226, + DOI 10.17487/RFC5226, May 2008, + <http://www.rfc-editor.org/info/rfc5226>. + + [RFC6194] Polk, T., Chen, L., Turner, S., and P. Hoffman, "Security + Considerations for the SHA-0 and SHA-1 Message-Digest + Algorithms", RFC 6194, DOI 10.17487/RFC6194, March 2011, + <http://www.rfc-editor.org/info/rfc6194>. + + [RFC5246] Dierks, T. and E. Rescorla, "The Transport Layer Security + (TLS) Protocol Version 1.2", RFC 5246, + DOI 10.17487/RFC5246, August 2008, + <http://www.rfc-editor.org/info/rfc5246>. + +Acknowledgements + + This document benefited from discussions on the KITTEN WG mailing + list. The author would like to specially thank Russ Allbery, Dave + Cridland, Shawn Emery, Stephen Farrell, Simon Josefsson, Pearl Liang, + Alexey Melnikov, Peter Saint-Andre, Robert Sparks, Martin Thompson, + and Nico Williams for their comments on this topic. + + + + + +Hansen Standards Track [Page 7] + +RFC 7677 SASL SCRAM-SHA-256/SCRAM-SHA-256-PLUS November 2015 + + +Author's Address + + Tony Hansen + AT&T Laboratories + 200 Laurel Ave. South + Middletown, NJ 07748 + United States + + Email: tony+scramsha256@maillennium.att.com + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Hansen Standards Track [Page 8] + diff --git a/src/common/libManageSieve/doc/rfc6234.txt b/src/common/libManageSieve/doc/rfc6234.txt new file mode 100644 index 00000000..814568ab --- /dev/null +++ b/src/common/libManageSieve/doc/rfc6234.txt @@ -0,0 +1,7115 @@ + + + + + + +Internet Engineering Task Force (IETF) D. Eastlake 3rd +Request for Comments: 6234 Huawei +Obsoletes: 4634 T. Hansen +Updates: 3174 AT&T Labs +Category: Informational May 2011 +ISSN: 2070-1721 + + + US Secure Hash Algorithms + (SHA and SHA-based HMAC and HKDF) + +Abstract + + The United States of America has adopted a suite of Secure Hash + Algorithms (SHAs), including four beyond SHA-1, as part of a Federal + Information Processing Standard (FIPS), namely SHA-224, SHA-256, + SHA-384, and SHA-512. This document makes open source code + performing these SHA hash functions conveniently available to the + Internet community. The sample code supports input strings of + arbitrary bit length. Much of the text herein was adapted by the + authors from FIPS 180-2. + + This document replaces RFC 4634, fixing errata and adding code for an + HMAC-based extract-and-expand Key Derivation Function, HKDF (RFC + 5869). As with RFC 4634, code to perform SHA-based Hashed Message + Authentication Codes (HMACs) is also included. + +Status of This Memo + + This document is not an Internet Standards Track specification; it is + published for informational purposes. + + This document is a product of the Internet Engineering Task Force + (IETF). It represents the consensus of the IETF community. It has + received public review and has been approved for publication by the + Internet Engineering Steering Group (IESG). Not all documents + approved by the IESG are a candidate for any level of Internet + Standard; see Section 2 of RFC 5741. + + Information about the current status of this document, any errata, + and how to provide feedback on it may be obtained at + http://www.rfc-editor.org/info/rfc6234. + + + + + + + + + +Eastlake & Hansen Informational [Page 1] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + +Copyright Notice + + Copyright (c) 2011 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents + (http://trustee.ietf.org/license-info) in effect on the date of + publication of this document. Please review these documents + carefully, as they describe your rights and restrictions with respect + to this document. Code Components extracted from this document must + include Simplified BSD License text as described in Section 4.e of + the Trust Legal Provisions and are provided without warranty as + described in the Simplified BSD License. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Eastlake & Hansen Informational [Page 2] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + +Table of Contents + + 1. Overview of Contents ............................................4 + 2. Notation for Bit Strings and Integers ...........................5 + 3. Operations on Words .............................................6 + 4. Message Padding and Parsing .....................................8 + 4.1. SHA-224 and SHA-256 ........................................8 + 4.2. SHA-384 and SHA-512 ........................................9 + 5. Functions and Constants Used ...................................10 + 5.1. SHA-224 and SHA-256 .......................................10 + 5.2. SHA-384 and SHA-512 .......................................11 + 6. Computing the Message Digest ...................................12 + 6.1. SHA-224 and SHA-256 Initialization ........................12 + 6.2. SHA-224 and SHA-256 Processing ............................13 + 6.3. SHA-384 and SHA-512 Initialization ........................14 + 6.4. SHA-384 and SHA-512 Processing ............................15 + 7. HKDF- and SHA-Based HMACs ......................................17 + 7.1. SHA-Based HMACs ...........................................17 + 7.2. HKDF ......................................................17 + 8. C Code for SHAs, HMAC, and HKDF ................................17 + 8.1. The Header Files ..........................................21 + 8.1.1. The .h file ........................................21 + 8.1.2. stdint-example.h ...................................29 + 8.1.3. sha-private.h ......................................29 + 8.2. The SHA Code ..............................................30 + 8.2.1. sha1.c .............................................30 + 8.2.2. sha224-256.c .......................................39 + 8.2.3. sha384-512.c .......................................51 + 8.2.4. usha.c .............................................73 + 8.3. The HMAC Code .............................................79 + 8.4. The HKDF Code .............................................84 + 8.5. The Test Driver ...........................................91 + 9. Security Considerations .......................................123 + 10. Acknowledgements .............................................123 + 11. References ...................................................124 + 11.1. Normative References ....................................124 + 11.2. Informative References ..................................124 + Appendix: Changes from RFC 4634...................................126 + + + + + + + + + + + + + +Eastlake & Hansen Informational [Page 3] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + +1. Overview of Contents + + This document includes specifications for the United States of + America (USA) Federal Information Processing Standard (FIPS) Secure + Hash Algorithms (SHAs), code to implement the SHAs, code to implement + HMAC (Hashed Message Authentication Code, [RFC2104]) based on the + SHAs, and code to implement HKDF (HMAC-based Key Derivation Function, + [RFC5869]) based on HMAC. Specifications for HMAC and HKDF are not + included as they appear elsewhere in the RFC series [RFC2104] + [RFC5869]. + + NOTE: Much of the text below is taken from [SHS], and the assertions + of the security of the hash algorithms described therein are made by + the US Government, the author of [SHS], not by the listed authors of + this document. See also [RFC6194] concerning the security of SHA-1. + + The text below specifies Secure Hash Algorithms, SHA-224 [RFC3874], + SHA-256, SHA-384, and SHA-512, for computing a condensed + representation of a message or a data file. (SHA-1 is specified in + [RFC3174].) When a message of any length < 2^64 bits (for SHA-224 and + SHA-256) or < 2^128 bits (for SHA-384 and SHA-512) is input to one of + these algorithms, the result is an output called a message digest. + The message digests range in length from 224 to 512 bits, depending + on the algorithm. Secure Hash Algorithms are typically used with + other cryptographic algorithms, such as digital signature algorithms + and keyed-hash authentication codes, the generation of random numbers + [RFC4086], or in key derivation functions. + + The algorithms specified in this document are called secure because + it is computationally infeasible to (1) find a message that + corresponds to a given message digest, or (2) find two different + messages that produce the same message digest. Any change to a + message in transit will, with very high probability, result in a + different message digest. This will result in a verification failure + when the Secure Hash Algorithm is used with a digital signature + algorithm or a keyed-hash message authentication algorithm. + + The code provided herein supports input strings of arbitrary bit + length. SHA-1's sample code from [RFC3174] has also been updated to + handle input strings of arbitrary bit length. Permission is granted + for all uses, commercial and non-commercial, of this code. + + This document obsoletes [RFC4634], and the changes from that RFC are + summarized in the Appendix. + + + + + + + +Eastlake & Hansen Informational [Page 4] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + ASN.1 OIDs (Object Identifiers) for the SHA algorithms, taken from + [RFC4055], are as follows: + + id-sha1 OBJECT IDENTIFIER ::= { iso(1) + identified-organization(3) oiw(14) + secsig(3) algorithms(2) 26 } + id-sha224 OBJECT IDENTIFIER ::= {{ joint-iso-itu-t(2) + country(16) us(840) organization(1) gov(101) + csor(3) nistalgorithm(4) hashalgs(2) 4 } + id-sha256 OBJECT IDENTIFIER ::= { joint-iso-itu-t(2) + country(16) us(840) organization(1) gov(101) + csor(3) nistalgorithm(4) hashalgs(2) 1 } + id-sha384 OBJECT IDENTIFIER ::= { joint-iso-itu-t(2) + country(16) us(840) organization(1) gov(101) + csor(3) nistalgorithm(4) hashalgs(2) 2 } + id-sha512 OBJECT IDENTIFIER ::= { joint-iso-itu-t(2) + country(16) us(840) organization(1) gov(101) + csor(3) nistalgorithm(4) hashalgs(2) 3 } + + Section 2 below defines the terminology and functions used as + building blocks to form these algorithms. Section 3 describes the + fundamental operations on words from which these algorithms are + built. Section 4 describes how messages are padded up to an integral + multiple of the required block size and then parsed into blocks. + Section 5 defines the constants and the composite functions used to + specify the hash algorithms. Section 6 gives the actual + specification for the SHA-224, SHA-256, SHA-384, and SHA-512 + functions. Section 7 provides pointers to the specification of HMAC + keyed message authentication codes and to the specification of an + extract-and-expand key derivation function based on HMAC. + + Section 8 gives sample code for the SHA algorithms, for SHA-based + HMACs, and for HMAC-based extract-and-expand key derivation function. + +2. Notation for Bit Strings and Integers + + The following terminology related to bit strings and integers will be + used: + + a. A hex digit is an element of the set {0, 1, ... , 9, A, ... , F}. + A hex digit is the representation of a 4-bit string. Examples: 7 + = 0111, A = 1010. + + b. A word equals a 32-bit or 64-bit string that may be represented + as a sequence of 8 or 16 hex digits, respectively. To convert a + word to hex digits, each 4-bit string is converted to its hex + equivalent as described in (a) above. Example: + + + + +Eastlake & Hansen Informational [Page 5] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + 1010 0001 0000 0011 1111 1110 0010 0011 = A103FE23. + + Throughout this document, the "big-endian" convention is used when + expressing both 32-bit and 64-bit words, so that within each word + the most significant bit is shown in the leftmost bit position. + + c. An integer may be represented as a word or pair of words. + + An integer between 0 and 2^32 - 1 inclusive may be represented as + a 32-bit word. The least significant four bits of the integer are + represented by the rightmost hex digit of the word representation. + Example: the integer 291 = 2^8+2^5+2^1+2^0 = 256+32+2+1 is + represented by the hex word 00000123. + + The same holds true for an integer between 0 and 2^64-1 inclusive, + which may be represented as a 64-bit word. + + If Z is an integer, 0 <= z < 2^64, then z = (2^32)x + y where + 0 <= x < 2^32 and 0 <= y < 2^32. Since x and y can be represented + as words X and Y, respectively, z can be represented as the pair + of words (X,Y). + + Again, the "big-endian" convention is used and the most + significant word is in the leftmost word position for values + represented by multiple-words. + + d. block = 512-bit or 1024-bit string. A block (e.g., B) may be + represented as a sequence of 32-bit or 64-bit words. + +3. Operations on Words + + The following logical operators will be applied to words in all four + hash operations specified herein. SHA-224 and SHA-256 operate on + 32-bit words while SHA-384 and SHA-512 operate on 64-bit words. + + In the operations below, x<<n is obtained as follows: discard the + leftmost n bits of x and then pad the result with n zeroed bits on + the right (the result will still be the same number of bits). + Similarly, x>>n is obtained as follows: discard the rightmost n bits + of x and then prepend the result with n zeroed bits on the left (the + result will still be the same number of bits). + + a. Bitwise logical word operations + + X AND Y = bitwise logical "and" of X and Y. + + X OR Y = bitwise logical "inclusive-or" of X and Y. + + + + +Eastlake & Hansen Informational [Page 6] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + X XOR Y = bitwise logical "exclusive-or" of X and Y. + + NOT X = bitwise logical "complement" of X. + + Example: + 01101100101110011101001001111011 + XOR 01100101110000010110100110110111 + -------------------------------- + = 00001001011110001011101111001100 + + b. The operation X + Y is defined as follows: words X and Y represent + w-bit integers x and y, where 0 <= x < 2^w and 0 <= y < 2^w. For + positive integers n and m, let + + n mod m + + be the remainder upon dividing n by m. Compute + + z = (x + y) mod 2^w. + + Then 0 <= z < 2^w. Convert z to a word, Z, and define Z = X + Y. + + c. The right shift operation SHR^n(x), where x is a w-bit word and n + is an integer with 0 <= n < w, is defined by + + SHR^n(x) = x>>n + + d. The rotate right (circular right shift) operation ROTR^n(x), where + x is a w-bit word and n is an integer with 0 <= n < w, is defined + by + + ROTR^n(x) = (x>>n) OR (x<<(w-n)) + + e. The rotate left (circular left shift) operation ROTL^n(x), where x + is a w-bit word and n is an integer with 0 <= n < w, is defined by + + ROTL^n(X) = (x<<n) OR (x>>(w-n)) + + Note the following equivalence relationships, where w is fixed in + each relationship: + + ROTL^n(x) = ROTR^(w-n)(x) + + ROTR^n(x) = ROTL^(w-n)(x) + + + + + + + +Eastlake & Hansen Informational [Page 7] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + +4. Message Padding and Parsing + + The hash functions specified herein are used to compute a message + digest for a message or data file that is provided as input. The + message or data file should be considered to be a bit string. The + length of the message is the number of bits in the message (the empty + message has length 0). If the number of bits in a message is a + multiple of 8, for compactness we can represent the message in hex. + The purpose of message padding is to make the total length of a + padded message a multiple of 512 for SHA-224 and SHA-256 or a + multiple of 1024 for SHA-384 and SHA-512. + + The following specifies how this padding shall be performed. As a + summary, a "1" followed by m "0"s followed by a 64-bit or 128-bit + integer are appended to the end of the message to produce a padded + message of length 512*n or 1024*n. The appended integer is the + length of the original message. The padded message is then processed + by the hash function as n 512-bit or 1024-bit blocks. + +4.1. SHA-224 and SHA-256 + + Suppose a message has length L < 2^64. Before it is input to the + hash function, the message is padded on the right as follows: + + a. "1" is appended. Example: if the original message is "01010000", + this is padded to "010100001". + + b. K "0"s are appended where K is the smallest, non-negative solution + to the equation + + ( L + 1 + K ) mod 512 = 448 + + c. Then append the 64-bit block that is L in binary representation. + After appending this block, the length of the message will be a + multiple of 512 bits. + + Example: Suppose the original message is the bit string + + 01100001 01100010 01100011 01100100 01100101 + + After step (a) this gives + + 01100001 01100010 01100011 01100100 01100101 1 + + + + + + + + +Eastlake & Hansen Informational [Page 8] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + Since L = 40, the number of bits in the above is 41 and K = 407 + "0"s are appended, making the total now 448. This gives the + following in hex: + + 61626364 65800000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 + + The 64-bit representation of L = 40 is hex 00000000 00000028. + Hence the final padded message is the following hex + + 61626364 65800000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000028 + +4.2. SHA-384 and SHA-512 + + Suppose a message has length L < 2^128. Before it is input to the + hash function, the message is padded on the right as follows: + + a. "1" is appended. Example: if the original message is "01010000", + this is padded to "010100001". + + b. K "0"s are appended where K is the smallest, non-negative solution + to the equation + + ( L + 1 + K ) mod 1024 = 896 + + c. Then append the 128-bit block that is L in binary representation. + After appending this block, the length of the message will be a + multiple of 1024 bits. + + Example: Suppose the original message is the bit string + + 01100001 01100010 01100011 01100100 01100101 + + After step (a) this gives + + 01100001 01100010 01100011 01100100 01100101 1 + + + + + + + + + + +Eastlake & Hansen Informational [Page 9] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + Since L = 40, the number of bits in the above is 41 and K = 855 + "0"s are appended, making the total now 896. This gives the + following in hex: + + 61626364 65800000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + + The 128-bit representation of L = 40 is hex 00000000 00000000 + 00000000 00000028. Hence the final padded message is the + following hex: + + 61626364 65800000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000000 + 00000000 00000000 00000000 00000028 + +5. Functions and Constants Used + + The following subsections give the six logical functions and the + table of constants used in each of the hash functions. + +5.1. SHA-224 and SHA-256 + + SHA-224 and SHA-256 use six logical functions, where each function + operates on 32-bit words, which are represented as x, y, and z. The + result of each function is a new 32-bit word. + + CH( x, y, z) = (x AND y) XOR ( (NOT x) AND z) + + MAJ( x, y, z) = (x AND y) XOR (x AND z) XOR (y AND z) + + BSIG0(x) = ROTR^2(x) XOR ROTR^13(x) XOR ROTR^22(x) + + BSIG1(x) = ROTR^6(x) XOR ROTR^11(x) XOR ROTR^25(x) + + SSIG0(x) = ROTR^7(x) XOR ROTR^18(x) XOR SHR^3(x) + + SSIG1(x) = ROTR^17(x) XOR ROTR^19(x) XOR SHR^10(x) + + + + +Eastlake & Hansen Informational [Page 10] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + SHA-224 and SHA-256 use the same sequence of sixty-four constant + 32-bit words, K0, K1, ..., K63. These words represent the first 32 + bits of the fractional parts of the cube roots of the first sixty- + four prime numbers. In hex, these constant words are as follows + (from left to right): + + 428a2f98 71374491 b5c0fbcf e9b5dba5 + 3956c25b 59f111f1 923f82a4 ab1c5ed5 + d807aa98 12835b01 243185be 550c7dc3 + 72be5d74 80deb1fe 9bdc06a7 c19bf174 + e49b69c1 efbe4786 0fc19dc6 240ca1cc + 2de92c6f 4a7484aa 5cb0a9dc 76f988da + 983e5152 a831c66d b00327c8 bf597fc7 + c6e00bf3 d5a79147 06ca6351 14292967 + 27b70a85 2e1b2138 4d2c6dfc 53380d13 + 650a7354 766a0abb 81c2c92e 92722c85 + a2bfe8a1 a81a664b c24b8b70 c76c51a3 + d192e819 d6990624 f40e3585 106aa070 + 19a4c116 1e376c08 2748774c 34b0bcb5 + 391c0cb3 4ed8aa4a 5b9cca4f 682e6ff3 + 748f82ee 78a5636f 84c87814 8cc70208 + 90befffa a4506ceb bef9a3f7 c67178f2 + +5.2. SHA-384 and SHA-512 + + SHA-384 and SHA-512 each use six logical functions, where each + function operates on 64-bit words, which are represented as x, y, and + z. The result of each function is a new 64-bit word. + + CH( x, y, z) = (x AND y) XOR ( (NOT x) AND z) + + MAJ( x, y, z) = (x AND y) XOR (x AND z) XOR (y AND z) + + BSIG0(x) = ROTR^28(x) XOR ROTR^34(x) XOR ROTR^39(x) + + BSIG1(x) = ROTR^14(x) XOR ROTR^18(x) XOR ROTR^41(x) + + SSIG0(x) = ROTR^1(x) XOR ROTR^8(x) XOR SHR^7(x) + + SSIG1(x) = ROTR^19(x) XOR ROTR^61(x) XOR SHR^6(x) + + SHA-384 and SHA-512 use the same sequence of eighty constant 64-bit + words, K0, K1, ... K79. These words represent the first 64 bits of + the fractional parts of the cube roots of the first eighty prime + numbers. In hex, these constant words are as follows (from left to + right): + + + + + +Eastlake & Hansen Informational [Page 11] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + 428a2f98d728ae22 7137449123ef65cd b5c0fbcfec4d3b2f e9b5dba58189dbbc + 3956c25bf348b538 59f111f1b605d019 923f82a4af194f9b ab1c5ed5da6d8118 + d807aa98a3030242 12835b0145706fbe 243185be4ee4b28c 550c7dc3d5ffb4e2 + 72be5d74f27b896f 80deb1fe3b1696b1 9bdc06a725c71235 c19bf174cf692694 + e49b69c19ef14ad2 efbe4786384f25e3 0fc19dc68b8cd5b5 240ca1cc77ac9c65 + 2de92c6f592b0275 4a7484aa6ea6e483 5cb0a9dcbd41fbd4 76f988da831153b5 + 983e5152ee66dfab a831c66d2db43210 b00327c898fb213f bf597fc7beef0ee4 + c6e00bf33da88fc2 d5a79147930aa725 06ca6351e003826f 142929670a0e6e70 + 27b70a8546d22ffc 2e1b21385c26c926 4d2c6dfc5ac42aed 53380d139d95b3df + 650a73548baf63de 766a0abb3c77b2a8 81c2c92e47edaee6 92722c851482353b + a2bfe8a14cf10364 a81a664bbc423001 c24b8b70d0f89791 c76c51a30654be30 + d192e819d6ef5218 d69906245565a910 f40e35855771202a 106aa07032bbd1b8 + 19a4c116b8d2d0c8 1e376c085141ab53 2748774cdf8eeb99 34b0bcb5e19b48a8 + 391c0cb3c5c95a63 4ed8aa4ae3418acb 5b9cca4f7763e373 682e6ff3d6b2b8a3 + 748f82ee5defb2fc 78a5636f43172f60 84c87814a1f0ab72 8cc702081a6439ec + 90befffa23631e28 a4506cebde82bde9 bef9a3f7b2c67915 c67178f2e372532b + ca273eceea26619c d186b8c721c0c207 eada7dd6cde0eb1e f57d4f7fee6ed178 + 06f067aa72176fba 0a637dc5a2c898a6 113f9804bef90dae 1b710b35131c471b + 28db77f523047d84 32caab7b40c72493 3c9ebe0a15c9bebc 431d67c49c100d4c + 4cc5d4becb3e42b6 597f299cfc657e2a 5fcb6fab3ad6faec 6c44198c4a475817 + +6. Computing the Message Digest + + The output of each of the secure hash functions, after being applied + to a message of N blocks, is the hash quantity H(N). For SHA-224 and + SHA-256, H(i) can be considered to be eight 32-bit words, H(i)0, + H(i)1, ... H(i)7. For SHA-384 and SHA-512, it can be considered to + be eight 64-bit words, H(i)0, H(i)1, ..., H(i)7. + + As described below, the hash words are initialized, modified as each + message block is processed, and finally concatenated after processing + the last block to yield the output. For SHA-256 and SHA-512, all of + the H(N) variables are concatenated while the SHA-224 and SHA-384 + hashes are produced by omitting some from the final concatenation. + +6.1. SHA-224 and SHA-256 Initialization + + For SHA-224, the initial hash value, H(0), consists of the following + 32-bit words in hex: + + H(0)0 = c1059ed8 + H(0)1 = 367cd507 + H(0)2 = 3070dd17 + H(0)3 = f70e5939 + H(0)4 = ffc00b31 + H(0)5 = 68581511 + H(0)6 = 64f98fa7 + H(0)7 = befa4fa4 + + + +Eastlake & Hansen Informational [Page 12] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + For SHA-256, the initial hash value, H(0), consists of the following + eight 32-bit words, in hex. These words were obtained by taking the + first 32 bits of the fractional parts of the square roots of the + first eight prime numbers. + + H(0)0 = 6a09e667 + H(0)1 = bb67ae85 + H(0)2 = 3c6ef372 + H(0)3 = a54ff53a + H(0)4 = 510e527f + H(0)5 = 9b05688c + H(0)6 = 1f83d9ab + H(0)7 = 5be0cd19 + +6.2. SHA-224 and SHA-256 Processing + + SHA-224 and SHA-256 perform identical processing on message blocks + and differ only in how H(0) is initialized and how they produce their + final output. They may be used to hash a message, M, having a length + of L bits, where 0 <= L < 2^64. The algorithm uses (1) a message + schedule of sixty-four 32-bit words, (2) eight working variables of + 32 bits each, and (3) a hash value of eight 32-bit words. + + The words of the message schedule are labeled W0, W1, ..., W63. The + eight working variables are labeled a, b, c, d, e, f, g, and h. The + words of the hash value are labeled H(i)0, H(i)1, ..., H(i)7, which + will hold the initial hash value, H(0), replaced by each successive + intermediate hash value (after each message block is processed), + H(i), and ending with the final hash value, H(N), after all N blocks + are processed. They also use two temporary words, T1 and T2. + + The input message is padded as described in Section 4.1 above, then + parsed into 512-bit blocks that are considered to be composed of + sixteen 32-bit words M(i)0, M(i)1, ..., M(i)15. The following + computations are then performed for each of the N message blocks. + All addition is performed modulo 2^32. + + For i = 1 to N + + 1. Prepare the message schedule W: + For t = 0 to 15 + Wt = M(i)t + For t = 16 to 63 + Wt = SSIG1(W(t-2)) + W(t-7) + SSIG0(w(t-15)) + W(t-16) + + + + + + + +Eastlake & Hansen Informational [Page 13] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + 2. Initialize the working variables: + a = H(i-1)0 + b = H(i-1)1 + c = H(i-1)2 + d = H(i-1)3 + e = H(i-1)4 + f = H(i-1)5 + g = H(i-1)6 + h = H(i-1)7 + + 3. Perform the main hash computation: + For t = 0 to 63 + T1 = h + BSIG1(e) + CH(e,f,g) + Kt + Wt + T2 = BSIG0(a) + MAJ(a,b,c) + h = g + g = f + f = e + e = d + T1 + d = c + c = b + b = a + a = T1 + T2 + + 4. Compute the intermediate hash value H(i) + H(i)0 = a + H(i-1)0 + H(i)1 = b + H(i-1)1 + H(i)2 = c + H(i-1)2 + H(i)3 = d + H(i-1)3 + H(i)4 = e + H(i-1)4 + H(i)5 = f + H(i-1)5 + H(i)6 = g + H(i-1)6 + H(i)7 = h + H(i-1)7 + + After the above computations have been sequentially performed for all + of the blocks in the message, the final output is calculated. For + SHA-256, this is the concatenation of all of H(N)0, H(N)1, through + H(N)7. For SHA-224, this is the concatenation of H(N)0, H(N)1, + through H(N)6. + +6.3. SHA-384 and SHA-512 Initialization + + For SHA-384, the initial hash value, H(0), consists of the following + eight 64-bit words, in hex. These words were obtained by taking the + first 64 bits of the fractional parts of the square roots of the + ninth through sixteenth prime numbers. + + + + + + +Eastlake & Hansen Informational [Page 14] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + H(0)0 = cbbb9d5dc1059ed8 + H(0)1 = 629a292a367cd507 + H(0)2 = 9159015a3070dd17 + H(0)3 = 152fecd8f70e5939 + H(0)4 = 67332667ffc00b31 + H(0)5 = 8eb44a8768581511 + H(0)6 = db0c2e0d64f98fa7 + H(0)7 = 47b5481dbefa4fa4 + + For SHA-512, the initial hash value, H(0), consists of the following + eight 64-bit words, in hex. These words were obtained by taking the + first 64 bits of the fractional parts of the square roots of the + first eight prime numbers. + + H(0)0 = 6a09e667f3bcc908 + H(0)1 = bb67ae8584caa73b + H(0)2 = 3c6ef372fe94f82b + H(0)3 = a54ff53a5f1d36f1 + H(0)4 = 510e527fade682d1 + H(0)5 = 9b05688c2b3e6c1f + H(0)6 = 1f83d9abfb41bd6b + H(0)7 = 5be0cd19137e2179 + +6.4. SHA-384 and SHA-512 Processing + + SHA-384 and SHA-512 perform identical processing on message blocks + and differ only in how H(0) is initialized and how they produce their + final output. They may be used to hash a message, M, having a length + of L bits, where 0 <= L < 2^128. The algorithm uses (1) a message + schedule of eighty 64-bit words, (2) eight working variables of 64 + bits each, and (3) a hash value of eight 64-bit words. + + The words of the message schedule are labeled W0, W1, ..., W79. The + eight working variables are labeled a, b, c, d, e, f, g, and h. The + words of the hash value are labeled H(i)0, H(i)1, ..., H(i)7, which + will hold the initial hash value, H(0), replaced by each successive + intermediate hash value (after each message block is processed), + H(i), and ending with the final hash value, H(N) after all N blocks + are processed. + + The input message is padded as described in Section 4.2 above, then + parsed into 1024-bit blocks that are considered to be composed of + sixteen 64-bit words M(i)0, M(i)1, ..., M(i)15. The following + computations are then performed for each of the N message blocks. + All addition is performed modulo 2^64. + + + + + + +Eastlake & Hansen Informational [Page 15] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + For i = 1 to N + + 1. Prepare the message schedule W: + For t = 0 to 15 + Wt = M(i)t + For t = 16 to 79 + Wt = SSIG1(W(t-2)) + W(t-7) + SSIG0(W(t-15)) + W(t-16) + + 2. Initialize the working variables: + a = H(i-1)0 + b = H(i-1)1 + c = H(i-1)2 + d = H(i-1)3 + e = H(i-1)4 + f = H(i-1)5 + g = H(i-1)6 + h = H(i-1)7 + + 3. Perform the main hash computation: + For t = 0 to 79 + T1 = h + BSIG1(e) + CH(e,f,g) + Kt + Wt + T2 = BSIG0(a) + MAJ(a,b,c) + h = g + g = f + f = e + e = d + T1 + d = c + c = b + b = a + a = T1 + T2 + + 4. Compute the intermediate hash value H(i) + H(i)0 = a + H(i-1)0 + H(i)1 = b + H(i-1)1 + H(i)2 = c + H(i-1)2 + H(i)3 = d + H(i-1)3 + H(i)4 = e + H(i-1)4 + H(i)5 = f + H(i-1)5 + H(i)6 = g + H(i-1)6 + H(i)7 = h + H(i-1)7 + + After the above computations have been sequentially performed for all + of the blocks in the message, the final output is calculated. For + SHA-512, this is the concatenation of all of H(N)0, H(N)1, through + H(N)7. For SHA-384, this is the concatenation of H(N)0, H(N)1, + through H(N)5. + + + + + +Eastlake & Hansen Informational [Page 16] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + +7. HKDF- and SHA-Based HMACs + + Below are brief descriptions and pointers to more complete + descriptions and code for (1) SHA-based HMACs and (2) an HMAC-based + extract-and-expand key derivation function. Both HKDF and HMAC were + devised by Hugo Krawczyk. + +7.1. SHA-Based HMACs + + HMAC is a method for computing a keyed MAC (Message Authentication + Code) using a hash function as described in [RFC2104]. It uses a key + to mix in with the input text to produce the final hash. + + Sample code is also provided, in Section 8.3 below, to perform HMAC + based on any of the SHA algorithms described herein. The sample code + found in [RFC2104] was written in terms of a specified text size. + Since SHA is defined in terms of an arbitrary number of bits, the + sample HMAC code has been written to allow the text input to HMAC to + have an arbitrary number of octets and bits. A fixed-length + interface is also provided. + +7.2. HKDF + + HKDF is a specific Key Derivation Function (KDF), that is, a function + of initial keying material from which the KDF derives one or more + cryptographically strong secret keys. HKDF, which is described in + [RFC5869], is based on HMAC. + + Sample code for HKDF is provided in Section 8.4 below. + +8. C Code for SHAs, HMAC, and HKDF + + Below is a demonstration implementation of these secure hash + functions in C. Section 8.1 contains the header file sha.h that + declares all constants, structures, and functions used by the SHA and + HMAC functions. It includes conditionals based on the state of + definition of USE_32BIT_ONLY that, if that symbol is defined at + compile time, avoids 64-bit operations. It also contains sha- + private.h that provides some declarations common to all the SHA + functions. Section 8.2 contains the C code for sha1.c, sha224-256.c, + sha384-512.c, and usha.c. Section 8.3 contains the C code for the + HMAC functions, and Section 8.4 contains the C code for HKDF. + Section 8.5 contains a test driver to exercise the code. + + For each of the digest lengths $$$, there is the following set of + constants, a structure, and functions: + + + + + +Eastlake & Hansen Informational [Page 17] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + Constants: + SHA$$$HashSize number of octets in the hash + SHA$$$HashSizeBits number of bits in the hash + SHA$$$_Message_Block_Size + number of octets used in the intermediate + message blocks + Most functions return an enum value that is one of: + shaSuccess(0) on success + shaNull(1) when presented with a null pointer parameter + shaInputTooLong(2) when the input data is too long + shaStateError(3) when SHA$$$Input is called after + SHA$$$FinalBits or SHA$$$Result + + Structure: + typedef SHA$$$Context + an opaque structure holding the complete state + for producing the hash + + Functions: + int SHA$$$Reset(SHA$$$Context *context); + Reset the hash context state. + int SHA$$$Input(SHA$$$Context *context, const uint8_t *octets, + unsigned int bytecount); + Incorporate bytecount octets into the hash. + int SHA$$$FinalBits(SHA$$$Context *, const uint8_t octet, + unsigned int bitcount); + Incorporate bitcount bits into the hash. The bits are in + the upper portion of the octet. SHA$$$Input() cannot be + called after this. + int SHA$$$Result(SHA$$$Context *, + uint8_t Message_Digest[SHA$$$HashSize]); + Do the final calculations on the hash and copy the value + into Message_Digest. + + In addition, functions with the prefix USHA are provided that take a + SHAversion value (SHA$$$) to select the SHA function suite. They add + the following constants, structure, and functions: + + Constants: + shaBadParam(4) constant returned by USHA functions when + presented with a bad SHAversion (SHA$$$) + parameter or other illegal parameter values + USAMaxHashSize maximum of the SHA hash sizes + SHA$$$ SHAversion enumeration values, used by USHA, + HMAC, and HKDF functions to select the SHA + function suite + + + + + +Eastlake & Hansen Informational [Page 18] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + Structure: + typedef USHAContext + an opaque structure holding the complete state + for producing the hash + + Functions: + int USHAReset(USHAContext *context, SHAversion whichSha); + Reset the hash context state. + int USHAInput(USHAContext context*, + const uint8_t *bytes, unsigned int bytecount); + Incorporate bytecount octets into the hash. + int USHAFinalBits(USHAContext *context, + const uint8_t bits, unsigned int bitcount); + Incorporate bitcount bits into the hash. + int USHAResult(USHAContext *context, + uint8_t Message_Digest[USHAMaxHashSize]); + Do the final calculations on the hash and copy the value + into Message_Digest. Octets in Message_Digest beyond + USHAHashSize(whichSha) are left untouched. + int USHAHashSize(enum SHAversion whichSha); + The number of octets in the given hash. + int USHAHashSizeBits(enum SHAversion whichSha); + The number of bits in the given hash. + int USHABlockSize(enum SHAversion whichSha); + The internal block size for the given hash. + const char *USHAHashName(enum SHAversion whichSha); + This function will return the name of the given SHA + algorithm as a string. + + The HMAC functions follow the same pattern to allow any length of + text input to be used. + + Structure: + typedef HMACContext an opaque structure holding the complete state + for producing the keyed message digest (MAC) + + Functions: + int hmacReset(HMACContext *ctx, enum SHAversion whichSha, + const unsigned char *key, int key_len); + Reset the MAC context state. + int hmacInput(HMACContext *ctx, const unsigned char *text, + int text_len); + Incorporate text_len octets into the MAC. + int hmacFinalBits(HMACContext *ctx, const uint8_t bits, + unsigned int bitcount); + Incorporate bitcount bits into the MAC. + int hmacResult(HMACContext *ctx, + uint8_t Message_Digest[USHAMaxHashSize]); + + + +Eastlake & Hansen Informational [Page 19] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + Do the final calculations on the MAC and copy the value into + Message_Digest. Octets in Message_Digest beyond + USHAHashSize(whichSha) are left untouched. + + In addition, a combined interface is provided, similar to that shown + in [RFC2104], that allows a fixed-length text input to be used. + + int hmac(SHAversion whichSha, + const unsigned char *text, int text_len, + const unsigned char *key, int key_len, + uint8_t Message_Digest[USHAMaxHashSize]); + Calculate the given digest for the given text and key, and + return the resulting MAC. Octets in Message_Digest beyond + USHAHashSize(whichSha) are left untouched. + + The HKDF functions follow the same pattern to allow any length of + text input to be used. + + Structure: + typedef HKDFContext an opaque structure holding the complete state + for producing the keying material + Functions: + int hkdfReset(HKDFContext *context, enum SHAversion whichSha, + const unsigned char *salt, int salt_len) + Reset the key derivation state and initialize it with the + salt_len octets of the optional salt. + int hkdfInput(HKDFContext *context, const unsigned char *ikm, + int ikm_len) + Incorporate ikm_len octets into the entropy extractor. + int hkdfFinalBits(HKDFContext *context, uint8_t ikm_bits, + unsigned int ikm_bit_count) + Incorporate ikm_bit_count bits into the entropy extractor. + int hkdfResult(HKDFContext *context, + uint8_t prk[USHAMaxHashSize], + const unsigned char *info, int info_len, + uint8_t okm[ ], int okm_len) + Finish the HKDF extraction and perform the final HKDF + expansion, storing the okm_len octets into output keying + material (okm). Optionally store the pseudo-random key + (prk) that is generated internally. + + In addition, combined interfaces are provided, similar to that shown + in [RFC5869], that allows a fixed-length text input to be used. + + int hkdfExtract(SHAversion whichSha, + const unsigned char *salt, int salt_len, + const unsigned char *ikm, int ikm_len, + uint8_t prk[USHAMaxHashSize]) + + + +Eastlake & Hansen Informational [Page 20] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + Perform HKDF extraction, combining the salt_len octets of + the optional salt with the ikm_len octets of the input + keying material (ikm) to form the pseudo-random key prk. + The output prk must be large enough to hold the octets + appropriate for the given hash type. + + int hkdfExpand(SHAversion whichSha, + const uint8_t prk[ ], int prk_len, + const unsigned char *info, int info_len, + uint8_t okm[ ], int okm_len) + Perform HKDF expansion, combining the prk_len octets of the + pseudo-random key prk with the info_len octets of info to + form the okm_len octets stored in okm. + + int hkdf(SHAversion whichSha, + const unsigned char *salt, int salt_len, + const unsigned char *ikm, int ikm_len, + const unsigned char *info, int info_len, + uint8_t okm[ ], int okm_len) + This combined interface performs both HKDF extraction and + expansion. The variables are the same as in hkdfExtract() + and hkdfExpand(). + +8.1. The Header Files + +8.1.1. The .h file + + The following sha.h file, as stated in the comments within the file, + assumes that <stdint.h> is available on your system. If it is not, + you should change to including <stdint-example.h>, provided in + Section 8.1.2, or the like. + +/**************************** sha.h ****************************/ +/***************** See RFC 6234 for details. *******************/ +/* + Copyright (c) 2011 IETF Trust and the persons identified as + authors of the code. All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, are permitted provided that the following + conditions are met: + + - Redistributions of source code must retain the above + copyright notice, this list of conditions and + the following disclaimer. + + + + + + +Eastlake & Hansen Informational [Page 21] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + - Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + - Neither the name of Internet Society, IETF or IETF Trust, nor + the names of specific contributors, may be used to endorse or + promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +#ifndef _SHA_H_ +#define _SHA_H_ + +/* + * Description: + * This file implements the Secure Hash Algorithms + * as defined in the U.S. National Institute of Standards + * and Technology Federal Information Processing Standards + * Publication (FIPS PUB) 180-3 published in October 2008 + * and formerly defined in its predecessors, FIPS PUB 180-1 + * and FIP PUB 180-2. + * + * A combined document showing all algorithms is available at + * http://csrc.nist.gov/publications/fips/ + * fips180-3/fips180-3_final.pdf + * + * The five hashes are defined in these sizes: + * SHA-1 20 byte / 160 bit + * SHA-224 28 byte / 224 bit + * SHA-256 32 byte / 256 bit + * SHA-384 48 byte / 384 bit + * SHA-512 64 byte / 512 bit + * + + + + +Eastlake & Hansen Informational [Page 22] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + * Compilation Note: + * These files may be compiled with two options: + * USE_32BIT_ONLY - use 32-bit arithmetic only, for systems + * without 64-bit integers + * + * USE_MODIFIED_MACROS - use alternate form of the SHA_Ch() + * and SHA_Maj() macros that are equivalent + * and potentially faster on many systems + * + */ + +#include <stdint.h> +/* + * If you do not have the ISO standard stdint.h header file, then you + * must typedef the following: + * name meaning + * uint64_t unsigned 64-bit integer + * uint32_t unsigned 32-bit integer + * uint8_t unsigned 8-bit integer (i.e., unsigned char) + * int_least16_t integer of >= 16 bits + * + * See stdint-example.h + */ + +#ifndef _SHA_enum_ +#define _SHA_enum_ +/* + * All SHA functions return one of these values. + */ +enum { + shaSuccess = 0, + shaNull, /* Null pointer parameter */ + shaInputTooLong, /* input data too long */ + shaStateError, /* called Input after FinalBits or Result */ + shaBadParam /* passed a bad parameter */ +}; +#endif /* _SHA_enum_ */ + +/* + * These constants hold size information for each of the SHA + * hashing operations + */ +enum { + SHA1_Message_Block_Size = 64, SHA224_Message_Block_Size = 64, + SHA256_Message_Block_Size = 64, SHA384_Message_Block_Size = 128, + SHA512_Message_Block_Size = 128, + USHA_Max_Message_Block_Size = SHA512_Message_Block_Size, + + + + +Eastlake & Hansen Informational [Page 23] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + SHA1HashSize = 20, SHA224HashSize = 28, SHA256HashSize = 32, + SHA384HashSize = 48, SHA512HashSize = 64, + USHAMaxHashSize = SHA512HashSize, + + SHA1HashSizeBits = 160, SHA224HashSizeBits = 224, + SHA256HashSizeBits = 256, SHA384HashSizeBits = 384, + SHA512HashSizeBits = 512, USHAMaxHashSizeBits = SHA512HashSizeBits +}; + +/* + * These constants are used in the USHA (Unified SHA) functions. + */ +typedef enum SHAversion { + SHA1, SHA224, SHA256, SHA384, SHA512 +} SHAversion; + +/* + * This structure will hold context information for the SHA-1 + * hashing operation. + */ +typedef struct SHA1Context { + uint32_t Intermediate_Hash[SHA1HashSize/4]; /* Message Digest */ + + uint32_t Length_High; /* Message length in bits */ + uint32_t Length_Low; /* Message length in bits */ + + int_least16_t Message_Block_Index; /* Message_Block array index */ + /* 512-bit message blocks */ + uint8_t Message_Block[SHA1_Message_Block_Size]; + + int Computed; /* Is the hash computed? */ + int Corrupted; /* Cumulative corruption code */ +} SHA1Context; + +/* + * This structure will hold context information for the SHA-256 + * hashing operation. + */ +typedef struct SHA256Context { + uint32_t Intermediate_Hash[SHA256HashSize/4]; /* Message Digest */ + + uint32_t Length_High; /* Message length in bits */ + uint32_t Length_Low; /* Message length in bits */ + + int_least16_t Message_Block_Index; /* Message_Block array index */ + /* 512-bit message blocks */ + uint8_t Message_Block[SHA256_Message_Block_Size]; + + + + +Eastlake & Hansen Informational [Page 24] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + int Computed; /* Is the hash computed? */ + int Corrupted; /* Cumulative corruption code */ +} SHA256Context; + +/* + * This structure will hold context information for the SHA-512 + * hashing operation. + */ +typedef struct SHA512Context { +#ifdef USE_32BIT_ONLY + uint32_t Intermediate_Hash[SHA512HashSize/4]; /* Message Digest */ + uint32_t Length[4]; /* Message length in bits */ +#else /* !USE_32BIT_ONLY */ + uint64_t Intermediate_Hash[SHA512HashSize/8]; /* Message Digest */ + uint64_t Length_High, Length_Low; /* Message length in bits */ +#endif /* USE_32BIT_ONLY */ + + int_least16_t Message_Block_Index; /* Message_Block array index */ + /* 1024-bit message blocks */ + uint8_t Message_Block[SHA512_Message_Block_Size]; + + int Computed; /* Is the hash computed?*/ + int Corrupted; /* Cumulative corruption code */ +} SHA512Context; + +/* + * This structure will hold context information for the SHA-224 + * hashing operation. It uses the SHA-256 structure for computation. + */ +typedef struct SHA256Context SHA224Context; + +/* + * This structure will hold context information for the SHA-384 + * hashing operation. It uses the SHA-512 structure for computation. + */ +typedef struct SHA512Context SHA384Context; + +/* + * This structure holds context information for all SHA + * hashing operations. + */ +typedef struct USHAContext { + int whichSha; /* which SHA is being used */ + union { + SHA1Context sha1Context; + SHA224Context sha224Context; SHA256Context sha256Context; + SHA384Context sha384Context; SHA512Context sha512Context; + } ctx; + + + +Eastlake & Hansen Informational [Page 25] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + +} USHAContext; + +/* + * This structure will hold context information for the HMAC + * keyed-hashing operation. + */ +typedef struct HMACContext { + int whichSha; /* which SHA is being used */ + int hashSize; /* hash size of SHA being used */ + int blockSize; /* block size of SHA being used */ + USHAContext shaContext; /* SHA context */ + unsigned char k_opad[USHA_Max_Message_Block_Size]; + /* outer padding - key XORd with opad */ + int Computed; /* Is the MAC computed? */ + int Corrupted; /* Cumulative corruption code */ + +} HMACContext; + +/* + * This structure will hold context information for the HKDF + * extract-and-expand Key Derivation Functions. + */ +typedef struct HKDFContext { + int whichSha; /* which SHA is being used */ + HMACContext hmacContext; + int hashSize; /* hash size of SHA being used */ + unsigned char prk[USHAMaxHashSize]; + /* pseudo-random key - output of hkdfInput */ + int Computed; /* Is the key material computed? */ + int Corrupted; /* Cumulative corruption code */ +} HKDFContext; + +/* + * Function Prototypes + */ + +/* SHA-1 */ +extern int SHA1Reset(SHA1Context *); +extern int SHA1Input(SHA1Context *, const uint8_t *bytes, + unsigned int bytecount); +extern int SHA1FinalBits(SHA1Context *, uint8_t bits, + unsigned int bit_count); +extern int SHA1Result(SHA1Context *, + uint8_t Message_Digest[SHA1HashSize]); + + + + + + + +Eastlake & Hansen Informational [Page 26] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + +/* SHA-224 */ +extern int SHA224Reset(SHA224Context *); +extern int SHA224Input(SHA224Context *, const uint8_t *bytes, + unsigned int bytecount); +extern int SHA224FinalBits(SHA224Context *, uint8_t bits, + unsigned int bit_count); +extern int SHA224Result(SHA224Context *, + uint8_t Message_Digest[SHA224HashSize]); + +/* SHA-256 */ +extern int SHA256Reset(SHA256Context *); +extern int SHA256Input(SHA256Context *, const uint8_t *bytes, + unsigned int bytecount); +extern int SHA256FinalBits(SHA256Context *, uint8_t bits, + unsigned int bit_count); +extern int SHA256Result(SHA256Context *, + uint8_t Message_Digest[SHA256HashSize]); + +/* SHA-384 */ +extern int SHA384Reset(SHA384Context *); +extern int SHA384Input(SHA384Context *, const uint8_t *bytes, + unsigned int bytecount); +extern int SHA384FinalBits(SHA384Context *, uint8_t bits, + unsigned int bit_count); +extern int SHA384Result(SHA384Context *, + uint8_t Message_Digest[SHA384HashSize]); + +/* SHA-512 */ +extern int SHA512Reset(SHA512Context *); +extern int SHA512Input(SHA512Context *, const uint8_t *bytes, + unsigned int bytecount); +extern int SHA512FinalBits(SHA512Context *, uint8_t bits, + unsigned int bit_count); +extern int SHA512Result(SHA512Context *, + uint8_t Message_Digest[SHA512HashSize]); + +/* Unified SHA functions, chosen by whichSha */ +extern int USHAReset(USHAContext *context, SHAversion whichSha); +extern int USHAInput(USHAContext *context, + const uint8_t *bytes, unsigned int bytecount); +extern int USHAFinalBits(USHAContext *context, + uint8_t bits, unsigned int bit_count); +extern int USHAResult(USHAContext *context, + uint8_t Message_Digest[USHAMaxHashSize]); +extern int USHABlockSize(enum SHAversion whichSha); +extern int USHAHashSize(enum SHAversion whichSha); +extern int USHAHashSizeBits(enum SHAversion whichSha); +extern const char *USHAHashName(enum SHAversion whichSha); + + + +Eastlake & Hansen Informational [Page 27] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + +/* + * HMAC Keyed-Hashing for Message Authentication, RFC 2104, + * for all SHAs. + * This interface allows a fixed-length text input to be used. + */ +extern int hmac(SHAversion whichSha, /* which SHA algorithm to use */ + const unsigned char *text, /* pointer to data stream */ + int text_len, /* length of data stream */ + const unsigned char *key, /* pointer to authentication key */ + int key_len, /* length of authentication key */ + uint8_t digest[USHAMaxHashSize]); /* caller digest to fill in */ + +/* + * HMAC Keyed-Hashing for Message Authentication, RFC 2104, + * for all SHAs. + * This interface allows any length of text input to be used. + */ +extern int hmacReset(HMACContext *context, enum SHAversion whichSha, + const unsigned char *key, int key_len); +extern int hmacInput(HMACContext *context, const unsigned char *text, + int text_len); +extern int hmacFinalBits(HMACContext *context, uint8_t bits, + unsigned int bit_count); +extern int hmacResult(HMACContext *context, + uint8_t digest[USHAMaxHashSize]); + +/* + * HKDF HMAC-based Extract-and-Expand Key Derivation Function, + * RFC 5869, for all SHAs. + */ +extern int hkdf(SHAversion whichSha, const unsigned char *salt, + int salt_len, const unsigned char *ikm, int ikm_len, + const unsigned char *info, int info_len, + uint8_t okm[ ], int okm_len); +extern int hkdfExtract(SHAversion whichSha, const unsigned char *salt, + int salt_len, const unsigned char *ikm, + int ikm_len, uint8_t prk[USHAMaxHashSize]); +extern int hkdfExpand(SHAversion whichSha, const uint8_t prk[ ], + int prk_len, const unsigned char *info, + int info_len, uint8_t okm[ ], int okm_len); + +/* + * HKDF HMAC-based Extract-and-Expand Key Derivation Function, + * RFC 5869, for all SHAs. + * This interface allows any length of text input to be used. + */ +extern int hkdfReset(HKDFContext *context, enum SHAversion whichSha, + const unsigned char *salt, int salt_len); + + + +Eastlake & Hansen Informational [Page 28] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + +extern int hkdfInput(HKDFContext *context, const unsigned char *ikm, + int ikm_len); +extern int hkdfFinalBits(HKDFContext *context, uint8_t ikm_bits, + unsigned int ikm_bit_count); +extern int hkdfResult(HKDFContext *context, + uint8_t prk[USHAMaxHashSize], + const unsigned char *info, int info_len, + uint8_t okm[USHAMaxHashSize], int okm_len); +#endif /* _SHA_H_ */ + +8.1.2. stdint-example.h + +If your system does not have <stdint.h>, the following should be +adequate as a substitute for compiling the other code in this document. + +/*********************** stdint-example.h **********************/ +/**** Use this file if your system does not have a stdint.h. ***/ +/***************** See RFC 6234 for details. *******************/ +#ifndef STDINT_H +#define STDINT_H + +typedef unsigned long long uint64_t; /* unsigned 64-bit integer */ +typedef unsigned int uint32_t; /* unsigned 32-bit integer */ +typedef unsigned char uint8_t; /* unsigned 8-bit integer */ + /* (i.e., unsigned char) */ +typedef int int_least32_t; /* integer of >= 32 bits */ +typedef short int_least16_t; /* integer of >= 16 bits */ + +#endif /* STDINT_H */ + +8.1.3. sha-private.h + + The sha-private.h header file contains definitions that should only + be needed internally in the other code in this document. These + definitions should not be needed in application code calling the code + provided in this document. + +/************************ sha-private.h ************************/ +/***************** See RFC 6234 for details. *******************/ +#ifndef _SHA_PRIVATE__H +#define _SHA_PRIVATE__H +/* + * These definitions are defined in FIPS 180-3, section 4.1. + * Ch() and Maj() are defined identically in sections 4.1.1, + * 4.1.2, and 4.1.3. + * + * The definitions used in FIPS 180-3 are as follows: + */ + + + +Eastlake & Hansen Informational [Page 29] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + +#ifndef USE_MODIFIED_MACROS +#define SHA_Ch(x,y,z) (((x) & (y)) ^ ((~(x)) & (z))) +#define SHA_Maj(x,y,z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z))) +#else /* USE_MODIFIED_MACROS */ +/* + * The following definitions are equivalent and potentially faster. + */ + +#define SHA_Ch(x, y, z) (((x) & ((y) ^ (z))) ^ (z)) +#define SHA_Maj(x, y, z) (((x) & ((y) | (z))) | ((y) & (z))) + +#endif /* USE_MODIFIED_MACROS */ + +#define SHA_Parity(x, y, z) ((x) ^ (y) ^ (z)) + +#endif /* _SHA_PRIVATE__H */ + +8.2. The SHA Code + + This code is primarily intended as expository reference code and + could be optimized further. For example, the assignment rotations + through the variables a, b, ..., h could be treated as a cycle and + the loop unrolled, rather than doing the explicit copying. + + Note that there are alternative representations of the Ch() and Maj() + functions controlled by an ifdef. + +8.2.1. sha1.c + +/**************************** sha1.c ***************************/ +/***************** See RFC 6234 for details. *******************/ +/* Copyright (c) 2011 IETF Trust and the persons identified as */ +/* authors of the code. All rights reserved. */ +/* See sha.h for terms of use and redistribution. */ + +/* + * Description: + * This file implements the Secure Hash Algorithm SHA-1 + * as defined in the U.S. National Institute of Standards + * and Technology Federal Information Processing Standards + * Publication (FIPS PUB) 180-3 published in October 2008 + * and formerly defined in its predecessors, FIPS PUB 180-1 + * and FIP PUB 180-2. + * + * A combined document showing all algorithms is available at + * http://csrc.nist.gov/publications/fips/ + * fips180-3/fips180-3_final.pdf + * + + + +Eastlake & Hansen Informational [Page 30] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + * The SHA-1 algorithm produces a 160-bit message digest for a + * given data stream that can serve as a means of providing a + * "fingerprint" for a message. + * + * Portability Issues: + * SHA-1 is defined in terms of 32-bit "words". This code + * uses <stdint.h> (included via "sha.h") to define 32- and + * 8-bit unsigned integer types. If your C compiler does + * not support 32-bit unsigned integers, this code is not + * appropriate. + * + * Caveats: + * SHA-1 is designed to work with messages less than 2^64 bits + * long. This implementation uses SHA1Input() to hash the bits + * that are a multiple of the size of an 8-bit octet, and then + * optionally uses SHA1FinalBits() to hash the final few bits of + * the input. + */ + +#include "sha.h" +#include "sha-private.h" + +/* + * Define the SHA1 circular left shift macro + */ +#define SHA1_ROTL(bits,word) \ + (((word) << (bits)) | ((word) >> (32-(bits)))) + +/* + * Add "length" to the length. + * Set Corrupted when overflow has occurred. + */ +static uint32_t addTemp; +#define SHA1AddLength(context, length) \ + (addTemp = (context)->Length_Low, \ + (context)->Corrupted = \ + (((context)->Length_Low += (length)) < addTemp) && \ + (++(context)->Length_High == 0) ? shaInputTooLong \ + : (context)->Corrupted ) + +/* Local Function Prototypes */ +static void SHA1ProcessMessageBlock(SHA1Context *context); +static void SHA1Finalize(SHA1Context *context, uint8_t Pad_Byte); +static void SHA1PadMessage(SHA1Context *context, uint8_t Pad_Byte); + + + + + + + +Eastlake & Hansen Informational [Page 31] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + +/* + * SHA1Reset + * + * Description: + * This function will initialize the SHA1Context in preparation + * for computing a new SHA1 message digest. + * + * Parameters: + * context: [in/out] + * The context to reset. + * + * Returns: + * sha Error Code. + * + */ +int SHA1Reset(SHA1Context *context) +{ + if (!context) return shaNull; + + context->Length_High = context->Length_Low = 0; + context->Message_Block_Index = 0; + + /* Initial Hash Values: FIPS 180-3 section 5.3.1 */ + context->Intermediate_Hash[0] = 0x67452301; + context->Intermediate_Hash[1] = 0xEFCDAB89; + context->Intermediate_Hash[2] = 0x98BADCFE; + context->Intermediate_Hash[3] = 0x10325476; + context->Intermediate_Hash[4] = 0xC3D2E1F0; + + context->Computed = 0; + context->Corrupted = shaSuccess; + + return shaSuccess; +} + +/* + * SHA1Input + * + * Description: + * This function accepts an array of octets as the next portion + * of the message. + * + * Parameters: + * context: [in/out] + * The SHA context to update. + * message_array[ ]: [in] + * An array of octets representing the next portion of + * the message. + + + +Eastlake & Hansen Informational [Page 32] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + * length: [in] + * The length of the message in message_array. + * + * Returns: + * sha Error Code. + * + */ +int SHA1Input(SHA1Context *context, + const uint8_t *message_array, unsigned length) +{ + if (!context) return shaNull; + if (!length) return shaSuccess; + if (!message_array) return shaNull; + if (context->Computed) return context->Corrupted = shaStateError; + if (context->Corrupted) return context->Corrupted; + + while (length--) { + context->Message_Block[context->Message_Block_Index++] = + *message_array; + + if ((SHA1AddLength(context, 8) == shaSuccess) && + (context->Message_Block_Index == SHA1_Message_Block_Size)) + SHA1ProcessMessageBlock(context); + + message_array++; + } + + return context->Corrupted; +} + +/* + * SHA1FinalBits + * + * Description: + * This function will add in any final bits of the message. + * + * Parameters: + * context: [in/out] + * The SHA context to update. + * message_bits: [in] + * The final bits of the message, in the upper portion of the + * byte. (Use 0b###00000 instead of 0b00000### to input the + * three bits ###.) + * length: [in] + * The number of bits in message_bits, between 1 and 7. + * + * Returns: + * sha Error Code. + + + +Eastlake & Hansen Informational [Page 33] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + */ +int SHA1FinalBits(SHA1Context *context, uint8_t message_bits, + unsigned int length) +{ + static uint8_t masks[8] = { + /* 0 0b00000000 */ 0x00, /* 1 0b10000000 */ 0x80, + /* 2 0b11000000 */ 0xC0, /* 3 0b11100000 */ 0xE0, + /* 4 0b11110000 */ 0xF0, /* 5 0b11111000 */ 0xF8, + /* 6 0b11111100 */ 0xFC, /* 7 0b11111110 */ 0xFE + }; + + static uint8_t markbit[8] = { + /* 0 0b10000000 */ 0x80, /* 1 0b01000000 */ 0x40, + /* 2 0b00100000 */ 0x20, /* 3 0b00010000 */ 0x10, + /* 4 0b00001000 */ 0x08, /* 5 0b00000100 */ 0x04, + /* 6 0b00000010 */ 0x02, /* 7 0b00000001 */ 0x01 + }; + + if (!context) return shaNull; + if (!length) return shaSuccess; + if (context->Corrupted) return context->Corrupted; + if (context->Computed) return context->Corrupted = shaStateError; + if (length >= 8) return context->Corrupted = shaBadParam; + + SHA1AddLength(context, length); + SHA1Finalize(context, + (uint8_t) ((message_bits & masks[length]) | markbit[length])); + + return context->Corrupted; +} + +/* + * SHA1Result + * + * Description: + * This function will return the 160-bit message digest + * into the Message_Digest array provided by the caller. + * NOTE: + * The first octet of hash is stored in the element with index 0, + * the last octet of hash in the element with index 19. + * + * Parameters: + * context: [in/out] + * The context to use to calculate the SHA-1 hash. + * Message_Digest[ ]: [out] + * Where the digest is returned. + * + + + + +Eastlake & Hansen Informational [Page 34] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + * Returns: + * sha Error Code. + * + */ +int SHA1Result(SHA1Context *context, + uint8_t Message_Digest[SHA1HashSize]) +{ + int i; + + if (!context) return shaNull; + if (!Message_Digest) return shaNull; + if (context->Corrupted) return context->Corrupted; + + if (!context->Computed) + SHA1Finalize(context, 0x80); + + for (i = 0; i < SHA1HashSize; ++i) + Message_Digest[i] = (uint8_t) (context->Intermediate_Hash[i>>2] + >> (8 * ( 3 - ( i & 0x03 ) ))); + + return shaSuccess; +} + +/* + * SHA1ProcessMessageBlock + * + * Description: + * This helper function will process the next 512 bits of the + * message stored in the Message_Block array. + * + * Parameters: + * context: [in/out] + * The SHA context to update. + * + * Returns: + * Nothing. + * + * Comments: + * Many of the variable names in this code, especially the + * single character names, were used because those were the + * names used in the Secure Hash Standard. + */ +static void SHA1ProcessMessageBlock(SHA1Context *context) +{ + /* Constants defined in FIPS 180-3, section 4.2.1 */ + const uint32_t K[4] = { + 0x5A827999, 0x6ED9EBA1, 0x8F1BBCDC, 0xCA62C1D6 + }; + + + +Eastlake & Hansen Informational [Page 35] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + int t; /* Loop counter */ + uint32_t temp; /* Temporary word value */ + uint32_t W[80]; /* Word sequence */ + uint32_t A, B, C, D, E; /* Word buffers */ + + /* + * Initialize the first 16 words in the array W + */ + for (t = 0; t < 16; t++) { + W[t] = ((uint32_t)context->Message_Block[t * 4]) << 24; + W[t] |= ((uint32_t)context->Message_Block[t * 4 + 1]) << 16; + W[t] |= ((uint32_t)context->Message_Block[t * 4 + 2]) << 8; + W[t] |= ((uint32_t)context->Message_Block[t * 4 + 3]); + } + + for (t = 16; t < 80; t++) + W[t] = SHA1_ROTL(1, W[t-3] ^ W[t-8] ^ W[t-14] ^ W[t-16]); + + A = context->Intermediate_Hash[0]; + B = context->Intermediate_Hash[1]; + C = context->Intermediate_Hash[2]; + D = context->Intermediate_Hash[3]; + E = context->Intermediate_Hash[4]; + + for (t = 0; t < 20; t++) { + temp = SHA1_ROTL(5,A) + SHA_Ch(B, C, D) + E + W[t] + K[0]; + E = D; + D = C; + C = SHA1_ROTL(30,B); + B = A; + A = temp; + } + + for (t = 20; t < 40; t++) { + temp = SHA1_ROTL(5,A) + SHA_Parity(B, C, D) + E + W[t] + K[1]; + E = D; + D = C; + C = SHA1_ROTL(30,B); + B = A; + A = temp; + } + + for (t = 40; t < 60; t++) { + temp = SHA1_ROTL(5,A) + SHA_Maj(B, C, D) + E + W[t] + K[2]; + E = D; + D = C; + C = SHA1_ROTL(30,B); + B = A; + + + +Eastlake & Hansen Informational [Page 36] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + A = temp; + } + + for (t = 60; t < 80; t++) { + temp = SHA1_ROTL(5,A) + SHA_Parity(B, C, D) + E + W[t] + K[3]; + E = D; + D = C; + C = SHA1_ROTL(30,B); + B = A; + A = temp; + } + + context->Intermediate_Hash[0] += A; + context->Intermediate_Hash[1] += B; + context->Intermediate_Hash[2] += C; + context->Intermediate_Hash[3] += D; + context->Intermediate_Hash[4] += E; + context->Message_Block_Index = 0; +} + +/* + * SHA1Finalize + * + * Description: + * This helper function finishes off the digest calculations. + * + * Parameters: + * context: [in/out] + * The SHA context to update. + * Pad_Byte: [in] + * The last byte to add to the message block before the 0-padding + * and length. This will contain the last bits of the message + * followed by another single bit. If the message was an + * exact multiple of 8-bits long, Pad_Byte will be 0x80. + * + * Returns: + * sha Error Code. + * + */ +static void SHA1Finalize(SHA1Context *context, uint8_t Pad_Byte) +{ + int i; + SHA1PadMessage(context, Pad_Byte); + /* message may be sensitive, clear it out */ + for (i = 0; i < SHA1_Message_Block_Size; ++i) + context->Message_Block[i] = 0; + context->Length_High = 0; /* and clear length */ + context->Length_Low = 0; + + + +Eastlake & Hansen Informational [Page 37] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + context->Computed = 1; +} + +/* + * SHA1PadMessage + * + * Description: + * According to the standard, the message must be padded to the next + * even multiple of 512 bits. The first padding bit must be a '1'. + * The last 64 bits represent the length of the original message. + * All bits in between should be 0. This helper function will pad + * the message according to those rules by filling the Message_Block + * array accordingly. When it returns, it can be assumed that the + * message digest has been computed. + * + * Parameters: + * context: [in/out] + * The context to pad. + * Pad_Byte: [in] + * The last byte to add to the message block before the 0-padding + * and length. This will contain the last bits of the message + * followed by another single bit. If the message was an + * exact multiple of 8-bits long, Pad_Byte will be 0x80. + * + * Returns: + * Nothing. + */ +static void SHA1PadMessage(SHA1Context *context, uint8_t Pad_Byte) +{ + /* + * Check to see if the current message block is too small to hold + * the initial padding bits and length. If so, we will pad the + * block, process it, and then continue padding into a second + * block. + */ + if (context->Message_Block_Index >= (SHA1_Message_Block_Size - 8)) { + context->Message_Block[context->Message_Block_Index++] = Pad_Byte; + while (context->Message_Block_Index < SHA1_Message_Block_Size) + context->Message_Block[context->Message_Block_Index++] = 0; + + SHA1ProcessMessageBlock(context); + } else + context->Message_Block[context->Message_Block_Index++] = Pad_Byte; + + while (context->Message_Block_Index < (SHA1_Message_Block_Size - 8)) + context->Message_Block[context->Message_Block_Index++] = 0; + + + + + +Eastlake & Hansen Informational [Page 38] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + /* + * Store the message length as the last 8 octets + */ + context->Message_Block[56] = (uint8_t) (context->Length_High >> 24); + context->Message_Block[57] = (uint8_t) (context->Length_High >> 16); + context->Message_Block[58] = (uint8_t) (context->Length_High >> 8); + context->Message_Block[59] = (uint8_t) (context->Length_High); + context->Message_Block[60] = (uint8_t) (context->Length_Low >> 24); + context->Message_Block[61] = (uint8_t) (context->Length_Low >> 16); + context->Message_Block[62] = (uint8_t) (context->Length_Low >> 8); + context->Message_Block[63] = (uint8_t) (context->Length_Low); + + SHA1ProcessMessageBlock(context); +} + +8.2.2. sha224-256.c + +/************************* sha224-256.c ************************/ +/***************** See RFC 6234 for details. *******************/ +/* Copyright (c) 2011 IETF Trust and the persons identified as */ +/* authors of the code. All rights reserved. */ +/* See sha.h for terms of use and redistribution. */ + +/* + * Description: + * This file implements the Secure Hash Algorithms SHA-224 and + * SHA-256 as defined in the U.S. National Institute of Standards + * and Technology Federal Information Processing Standards + * Publication (FIPS PUB) 180-3 published in October 2008 + * and formerly defined in its predecessors, FIPS PUB 180-1 + * and FIP PUB 180-2. + * + * A combined document showing all algorithms is available at + * http://csrc.nist.gov/publications/fips/ + * fips180-3/fips180-3_final.pdf + * + * The SHA-224 and SHA-256 algorithms produce 224-bit and 256-bit + * message digests for a given data stream. It should take about + * 2**n steps to find a message with the same digest as a given + * message and 2**(n/2) to find any two messages with the same + * digest, when n is the digest size in bits. Therefore, this + * algorithm can serve as a means of providing a + * "fingerprint" for a message. + * + * Portability Issues: + * SHA-224 and SHA-256 are defined in terms of 32-bit "words". + * This code uses <stdint.h> (included via "sha.h") to define 32- + * and 8-bit unsigned integer types. If your C compiler does not + + + +Eastlake & Hansen Informational [Page 39] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + * support 32-bit unsigned integers, this code is not + * appropriate. + * + * Caveats: + * SHA-224 and SHA-256 are designed to work with messages less + * than 2^64 bits long. This implementation uses SHA224/256Input() + * to hash the bits that are a multiple of the size of an 8-bit + * octet, and then optionally uses SHA224/256FinalBits() + * to hash the final few bits of the input. + */ + +#include "sha.h" +#include "sha-private.h" + +/* Define the SHA shift, rotate left, and rotate right macros */ +#define SHA256_SHR(bits,word) ((word) >> (bits)) +#define SHA256_ROTL(bits,word) \ + (((word) << (bits)) | ((word) >> (32-(bits)))) +#define SHA256_ROTR(bits,word) \ + (((word) >> (bits)) | ((word) << (32-(bits)))) + +/* Define the SHA SIGMA and sigma macros */ +#define SHA256_SIGMA0(word) \ + (SHA256_ROTR( 2,word) ^ SHA256_ROTR(13,word) ^ SHA256_ROTR(22,word)) +#define SHA256_SIGMA1(word) \ + (SHA256_ROTR( 6,word) ^ SHA256_ROTR(11,word) ^ SHA256_ROTR(25,word)) +#define SHA256_sigma0(word) \ + (SHA256_ROTR( 7,word) ^ SHA256_ROTR(18,word) ^ SHA256_SHR( 3,word)) +#define SHA256_sigma1(word) \ + (SHA256_ROTR(17,word) ^ SHA256_ROTR(19,word) ^ SHA256_SHR(10,word)) + +/* + * Add "length" to the length. + * Set Corrupted when overflow has occurred. + */ +static uint32_t addTemp; +#define SHA224_256AddLength(context, length) \ + (addTemp = (context)->Length_Low, (context)->Corrupted = \ + (((context)->Length_Low += (length)) < addTemp) && \ + (++(context)->Length_High == 0) ? shaInputTooLong : \ + (context)->Corrupted ) + +/* Local Function Prototypes */ +static int SHA224_256Reset(SHA256Context *context, uint32_t *H0); +static void SHA224_256ProcessMessageBlock(SHA256Context *context); +static void SHA224_256Finalize(SHA256Context *context, + uint8_t Pad_Byte); +static void SHA224_256PadMessage(SHA256Context *context, + + + +Eastlake & Hansen Informational [Page 40] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + uint8_t Pad_Byte); +static int SHA224_256ResultN(SHA256Context *context, + uint8_t Message_Digest[ ], int HashSize); + +/* Initial Hash Values: FIPS 180-3 section 5.3.2 */ +static uint32_t SHA224_H0[SHA256HashSize/4] = { + 0xC1059ED8, 0x367CD507, 0x3070DD17, 0xF70E5939, + 0xFFC00B31, 0x68581511, 0x64F98FA7, 0xBEFA4FA4 +}; + +/* Initial Hash Values: FIPS 180-3 section 5.3.3 */ +static uint32_t SHA256_H0[SHA256HashSize/4] = { + 0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, + 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19 +}; + +/* + * SHA224Reset + * + * Description: + * This function will initialize the SHA224Context in preparation + * for computing a new SHA224 message digest. + * + * Parameters: + * context: [in/out] + * The context to reset. + * + * Returns: + * sha Error Code. + */ +int SHA224Reset(SHA224Context *context) +{ + return SHA224_256Reset(context, SHA224_H0); +} + +/* + * SHA224Input + * + * Description: + * This function accepts an array of octets as the next portion + * of the message. + * + * Parameters: + * context: [in/out] + * The SHA context to update. + * message_array[ ]: [in] + * An array of octets representing the next portion of + * the message. + + + +Eastlake & Hansen Informational [Page 41] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + * length: [in] + * The length of the message in message_array. + * + * Returns: + * sha Error Code. + * + */ +int SHA224Input(SHA224Context *context, const uint8_t *message_array, + unsigned int length) +{ + return SHA256Input(context, message_array, length); +} + +/* + * SHA224FinalBits + * + * Description: + * This function will add in any final bits of the message. + * + * Parameters: + * context: [in/out] + * The SHA context to update. + * message_bits: [in] + * The final bits of the message, in the upper portion of the + * byte. (Use 0b###00000 instead of 0b00000### to input the + * three bits ###.) + * length: [in] + * The number of bits in message_bits, between 1 and 7. + * + * Returns: + * sha Error Code. + */ +int SHA224FinalBits(SHA224Context *context, + uint8_t message_bits, unsigned int length) +{ + return SHA256FinalBits(context, message_bits, length); +} + +/* + * SHA224Result + * + * Description: + * This function will return the 224-bit message digest + * into the Message_Digest array provided by the caller. + * NOTE: + * The first octet of hash is stored in the element with index 0, + * the last octet of hash in the element with index 27. + * + + + +Eastlake & Hansen Informational [Page 42] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + * Parameters: + * context: [in/out] + * The context to use to calculate the SHA hash. + * Message_Digest[ ]: [out] + * Where the digest is returned. + * + * Returns: + * sha Error Code. + */ +int SHA224Result(SHA224Context *context, + uint8_t Message_Digest[SHA224HashSize]) +{ + return SHA224_256ResultN(context, Message_Digest, SHA224HashSize); +} + +/* + * SHA256Reset + * + * Description: + * This function will initialize the SHA256Context in preparation + * for computing a new SHA256 message digest. + * + * Parameters: + * context: [in/out] + * The context to reset. + * + * Returns: + * sha Error Code. + */ +int SHA256Reset(SHA256Context *context) +{ + return SHA224_256Reset(context, SHA256_H0); +} + +/* + * SHA256Input + * + * Description: + * This function accepts an array of octets as the next portion + * of the message. + * + * Parameters: + * context: [in/out] + * The SHA context to update. + * message_array[ ]: [in] + * An array of octets representing the next portion of + * the message. + + + + +Eastlake & Hansen Informational [Page 43] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + * length: [in] + * The length of the message in message_array. + * + * Returns: + * sha Error Code. + */ +int SHA256Input(SHA256Context *context, const uint8_t *message_array, + unsigned int length) +{ + if (!context) return shaNull; + if (!length) return shaSuccess; + if (!message_array) return shaNull; + if (context->Computed) return context->Corrupted = shaStateError; + if (context->Corrupted) return context->Corrupted; + + while (length--) { + context->Message_Block[context->Message_Block_Index++] = + *message_array; + + if ((SHA224_256AddLength(context, 8) == shaSuccess) && + (context->Message_Block_Index == SHA256_Message_Block_Size)) + SHA224_256ProcessMessageBlock(context); + + message_array++; + } + + return context->Corrupted; + +} + +/* + * SHA256FinalBits + * + * Description: + * This function will add in any final bits of the message. + * + * Parameters: + * context: [in/out] + * The SHA context to update. + * message_bits: [in] + * The final bits of the message, in the upper portion of the + * byte. (Use 0b###00000 instead of 0b00000### to input the + * three bits ###.) + * length: [in] + * The number of bits in message_bits, between 1 and 7. + * + * Returns: + * sha Error Code. + + + +Eastlake & Hansen Informational [Page 44] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + */ +int SHA256FinalBits(SHA256Context *context, + uint8_t message_bits, unsigned int length) +{ + static uint8_t masks[8] = { + /* 0 0b00000000 */ 0x00, /* 1 0b10000000 */ 0x80, + /* 2 0b11000000 */ 0xC0, /* 3 0b11100000 */ 0xE0, + /* 4 0b11110000 */ 0xF0, /* 5 0b11111000 */ 0xF8, + /* 6 0b11111100 */ 0xFC, /* 7 0b11111110 */ 0xFE + }; + static uint8_t markbit[8] = { + /* 0 0b10000000 */ 0x80, /* 1 0b01000000 */ 0x40, + /* 2 0b00100000 */ 0x20, /* 3 0b00010000 */ 0x10, + /* 4 0b00001000 */ 0x08, /* 5 0b00000100 */ 0x04, + /* 6 0b00000010 */ 0x02, /* 7 0b00000001 */ 0x01 + }; + + if (!context) return shaNull; + if (!length) return shaSuccess; + if (context->Corrupted) return context->Corrupted; + if (context->Computed) return context->Corrupted = shaStateError; + if (length >= 8) return context->Corrupted = shaBadParam; + + SHA224_256AddLength(context, length); + SHA224_256Finalize(context, (uint8_t) + ((message_bits & masks[length]) | markbit[length])); + + return context->Corrupted; +} + +/* + * SHA256Result + * + * Description: + * This function will return the 256-bit message digest + * into the Message_Digest array provided by the caller. + * NOTE: + * The first octet of hash is stored in the element with index 0, + * the last octet of hash in the element with index 31. + * + * Parameters: + * context: [in/out] + * The context to use to calculate the SHA hash. + * Message_Digest[ ]: [out] + * Where the digest is returned. + * + * Returns: + * sha Error Code. + + + +Eastlake & Hansen Informational [Page 45] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + */ +int SHA256Result(SHA256Context *context, + uint8_t Message_Digest[SHA256HashSize]) +{ + return SHA224_256ResultN(context, Message_Digest, SHA256HashSize); +} + +/* + * SHA224_256Reset + * + * Description: + * This helper function will initialize the SHA256Context in + * preparation for computing a new SHA-224 or SHA-256 message digest. + * + * Parameters: + * context: [in/out] + * The context to reset. + * H0[ ]: [in] + * The initial hash value array to use. + * + * Returns: + * sha Error Code. + */ +static int SHA224_256Reset(SHA256Context *context, uint32_t *H0) +{ + if (!context) return shaNull; + + context->Length_High = context->Length_Low = 0; + context->Message_Block_Index = 0; + + context->Intermediate_Hash[0] = H0[0]; + context->Intermediate_Hash[1] = H0[1]; + context->Intermediate_Hash[2] = H0[2]; + context->Intermediate_Hash[3] = H0[3]; + context->Intermediate_Hash[4] = H0[4]; + context->Intermediate_Hash[5] = H0[5]; + context->Intermediate_Hash[6] = H0[6]; + context->Intermediate_Hash[7] = H0[7]; + + context->Computed = 0; + context->Corrupted = shaSuccess; + + return shaSuccess; +} + +/* + * SHA224_256ProcessMessageBlock + * + + + +Eastlake & Hansen Informational [Page 46] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + * Description: + * This helper function will process the next 512 bits of the + * message stored in the Message_Block array. + * + * Parameters: + * context: [in/out] + * The SHA context to update. + * + * Returns: + * Nothing. + * + * Comments: + * Many of the variable names in this code, especially the + * single character names, were used because those were the + * names used in the Secure Hash Standard. + */ +static void SHA224_256ProcessMessageBlock(SHA256Context *context) +{ + /* Constants defined in FIPS 180-3, section 4.2.2 */ + static const uint32_t K[64] = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, + 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, + 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, + 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, + 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, + 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, + 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, + 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, + 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, + 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, + 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, + 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 + }; + int t, t4; /* Loop counter */ + uint32_t temp1, temp2; /* Temporary word value */ + uint32_t W[64]; /* Word sequence */ + uint32_t A, B, C, D, E, F, G, H; /* Word buffers */ + + /* + * Initialize the first 16 words in the array W + */ + for (t = t4 = 0; t < 16; t++, t4 += 4) + W[t] = (((uint32_t)context->Message_Block[t4]) << 24) | + (((uint32_t)context->Message_Block[t4 + 1]) << 16) | + (((uint32_t)context->Message_Block[t4 + 2]) << 8) | + (((uint32_t)context->Message_Block[t4 + 3])); + + + + +Eastlake & Hansen Informational [Page 47] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + for (t = 16; t < 64; t++) + W[t] = SHA256_sigma1(W[t-2]) + W[t-7] + + SHA256_sigma0(W[t-15]) + W[t-16]; + + A = context->Intermediate_Hash[0]; + B = context->Intermediate_Hash[1]; + C = context->Intermediate_Hash[2]; + D = context->Intermediate_Hash[3]; + E = context->Intermediate_Hash[4]; + F = context->Intermediate_Hash[5]; + G = context->Intermediate_Hash[6]; + H = context->Intermediate_Hash[7]; + + for (t = 0; t < 64; t++) { + temp1 = H + SHA256_SIGMA1(E) + SHA_Ch(E,F,G) + K[t] + W[t]; + temp2 = SHA256_SIGMA0(A) + SHA_Maj(A,B,C); + H = G; + G = F; + F = E; + E = D + temp1; + D = C; + C = B; + B = A; + A = temp1 + temp2; + } + + context->Intermediate_Hash[0] += A; + context->Intermediate_Hash[1] += B; + context->Intermediate_Hash[2] += C; + context->Intermediate_Hash[3] += D; + context->Intermediate_Hash[4] += E; + context->Intermediate_Hash[5] += F; + context->Intermediate_Hash[6] += G; + context->Intermediate_Hash[7] += H; + + context->Message_Block_Index = 0; +} + +/* + * SHA224_256Finalize + * + * Description: + * This helper function finishes off the digest calculations. + * + * Parameters: + * context: [in/out] + * The SHA context to update. + * Pad_Byte: [in] + + + +Eastlake & Hansen Informational [Page 48] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + * The last byte to add to the message block before the 0-padding + * and length. This will contain the last bits of the message + * followed by another single bit. If the message was an + * exact multiple of 8-bits long, Pad_Byte will be 0x80. + * + * Returns: + * sha Error Code. + */ +static void SHA224_256Finalize(SHA256Context *context, + uint8_t Pad_Byte) +{ + int i; + SHA224_256PadMessage(context, Pad_Byte); + /* message may be sensitive, so clear it out */ + for (i = 0; i < SHA256_Message_Block_Size; ++i) + context->Message_Block[i] = 0; + context->Length_High = 0; /* and clear length */ + context->Length_Low = 0; + context->Computed = 1; +} + +/* + * SHA224_256PadMessage + * + * Description: + * According to the standard, the message must be padded to the next + * even multiple of 512 bits. The first padding bit must be a '1'. + * The last 64 bits represent the length of the original message. + * All bits in between should be 0. This helper function will pad + * the message according to those rules by filling the + * Message_Block array accordingly. When it returns, it can be + * assumed that the message digest has been computed. + * + * Parameters: + * context: [in/out] + * The context to pad. + * Pad_Byte: [in] + * The last byte to add to the message block before the 0-padding + * and length. This will contain the last bits of the message + * followed by another single bit. If the message was an + * exact multiple of 8-bits long, Pad_Byte will be 0x80. + * + * Returns: + * Nothing. + */ +static void SHA224_256PadMessage(SHA256Context *context, + uint8_t Pad_Byte) +{ + + + +Eastlake & Hansen Informational [Page 49] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + /* + * Check to see if the current message block is too small to hold + * the initial padding bits and length. If so, we will pad the + * block, process it, and then continue padding into a second + * block. + */ + if (context->Message_Block_Index >= (SHA256_Message_Block_Size-8)) { + context->Message_Block[context->Message_Block_Index++] = Pad_Byte; + while (context->Message_Block_Index < SHA256_Message_Block_Size) + context->Message_Block[context->Message_Block_Index++] = 0; + SHA224_256ProcessMessageBlock(context); + } else + context->Message_Block[context->Message_Block_Index++] = Pad_Byte; + + while (context->Message_Block_Index < (SHA256_Message_Block_Size-8)) + context->Message_Block[context->Message_Block_Index++] = 0; + + /* + * Store the message length as the last 8 octets + */ + context->Message_Block[56] = (uint8_t)(context->Length_High >> 24); + context->Message_Block[57] = (uint8_t)(context->Length_High >> 16); + context->Message_Block[58] = (uint8_t)(context->Length_High >> 8); + context->Message_Block[59] = (uint8_t)(context->Length_High); + context->Message_Block[60] = (uint8_t)(context->Length_Low >> 24); + context->Message_Block[61] = (uint8_t)(context->Length_Low >> 16); + context->Message_Block[62] = (uint8_t)(context->Length_Low >> 8); + context->Message_Block[63] = (uint8_t)(context->Length_Low); + + SHA224_256ProcessMessageBlock(context); +} + +/* + * SHA224_256ResultN + * + * Description: + * This helper function will return the 224-bit or 256-bit message + * digest into the Message_Digest array provided by the caller. + * NOTE: + * The first octet of hash is stored in the element with index 0, + * the last octet of hash in the element with index 27/31. + * + * Parameters: + * context: [in/out] + * The context to use to calculate the SHA hash. + * Message_Digest[ ]: [out] + * Where the digest is returned. + * HashSize: [in] + + + +Eastlake & Hansen Informational [Page 50] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + * The size of the hash, either 28 or 32. + * + * Returns: + * sha Error Code. + */ +static int SHA224_256ResultN(SHA256Context *context, + uint8_t Message_Digest[ ], int HashSize) +{ + int i; + + if (!context) return shaNull; + if (!Message_Digest) return shaNull; + if (context->Corrupted) return context->Corrupted; + + if (!context->Computed) + SHA224_256Finalize(context, 0x80); + + for (i = 0; i < HashSize; ++i) + Message_Digest[i] = (uint8_t) + (context->Intermediate_Hash[i>>2] >> 8 * ( 3 - ( i & 0x03 ) )); + + return shaSuccess; +} + +8.2.3. sha384-512.c + +/************************* sha384-512.c ************************/ +/***************** See RFC 6234 for details. *******************/ +/* Copyright (c) 2011 IETF Trust and the persons identified as */ +/* authors of the code. All rights reserved. */ +/* See sha.h for terms of use and redistribution. */ + +/* + * Description: + * This file implements the Secure Hash Algorithms SHA-384 and + * SHA-512 as defined in the U.S. National Institute of Standards + * and Technology Federal Information Processing Standards + * Publication (FIPS PUB) 180-3 published in October 2008 + * and formerly defined in its predecessors, FIPS PUB 180-1 + * and FIP PUB 180-2. + * + * A combined document showing all algorithms is available at + * http://csrc.nist.gov/publications/fips/ + * fips180-3/fips180-3_final.pdf + * + * The SHA-384 and SHA-512 algorithms produce 384-bit and 512-bit + * message digests for a given data stream. It should take about + * 2**n steps to find a message with the same digest as a given + + + +Eastlake & Hansen Informational [Page 51] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + * message and 2**(n/2) to find any two messages with the same + * digest, when n is the digest size in bits. Therefore, this + * algorithm can serve as a means of providing a + * "fingerprint" for a message. + * + * Portability Issues: + * SHA-384 and SHA-512 are defined in terms of 64-bit "words", + * but if USE_32BIT_ONLY is #defined, this code is implemented in + * terms of 32-bit "words". This code uses <stdint.h> (included + * via "sha.h") to define the 64-, 32- and 8-bit unsigned integer + * types. If your C compiler does not support 64-bit unsigned + * integers and you do not #define USE_32BIT_ONLY, this code is + * not appropriate. + * + * Caveats: + * SHA-384 and SHA-512 are designed to work with messages less + * than 2^128 bits long. This implementation uses SHA384/512Input() + * to hash the bits that are a multiple of the size of an 8-bit + * octet, and then optionally uses SHA384/256FinalBits() + * to hash the final few bits of the input. + * + */ + +#include "sha.h" + +#ifdef USE_32BIT_ONLY +/* + * Define 64-bit arithmetic in terms of 32-bit arithmetic. + * Each 64-bit number is represented in a 2-word array. + * All macros are defined such that the result is the last parameter. + */ + +/* + * Define shift, rotate left, and rotate right functions + */ +#define SHA512_SHR(bits, word, ret) ( \ + /* (((uint64_t)((word))) >> (bits)) */ \ + (ret)[0] = (((bits) < 32) && ((bits) >= 0)) ? \ + ((word)[0] >> (bits)) : 0, \ + (ret)[1] = ((bits) > 32) ? ((word)[0] >> ((bits) - 32)) : \ + ((bits) == 32) ? (word)[0] : \ + ((bits) >= 0) ? \ + (((word)[0] << (32 - (bits))) | \ + ((word)[1] >> (bits))) : 0 ) + +#define SHA512_SHL(bits, word, ret) ( \ + /* (((uint64_t)(word)) << (bits)) */ \ + (ret)[0] = ((bits) > 32) ? ((word)[1] << ((bits) - 32)) : \ + + + +Eastlake & Hansen Informational [Page 52] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + ((bits) == 32) ? (word)[1] : \ + ((bits) >= 0) ? \ + (((word)[0] << (bits)) | \ + ((word)[1] >> (32 - (bits)))) : \ + 0, \ + (ret)[1] = (((bits) < 32) && ((bits) >= 0)) ? \ + ((word)[1] << (bits)) : 0 ) + +/* + * Define 64-bit OR + */ +#define SHA512_OR(word1, word2, ret) ( \ + (ret)[0] = (word1)[0] | (word2)[0], \ + (ret)[1] = (word1)[1] | (word2)[1] ) + +/* + * Define 64-bit XOR + */ +#define SHA512_XOR(word1, word2, ret) ( \ + (ret)[0] = (word1)[0] ^ (word2)[0], \ + (ret)[1] = (word1)[1] ^ (word2)[1] ) + +/* + * Define 64-bit AND + */ +#define SHA512_AND(word1, word2, ret) ( \ + (ret)[0] = (word1)[0] & (word2)[0], \ + (ret)[1] = (word1)[1] & (word2)[1] ) + +/* + * Define 64-bit TILDA + */ +#define SHA512_TILDA(word, ret) \ + ( (ret)[0] = ~(word)[0], (ret)[1] = ~(word)[1] ) + +/* + * Define 64-bit ADD + */ +#define SHA512_ADD(word1, word2, ret) ( \ + (ret)[1] = (word1)[1], (ret)[1] += (word2)[1], \ + (ret)[0] = (word1)[0] + (word2)[0] + ((ret)[1] < (word1)[1]) ) + +/* + * Add the 4word value in word2 to word1. + */ +static uint32_t ADDTO4_temp, ADDTO4_temp2; +#define SHA512_ADDTO4(word1, word2) ( \ + ADDTO4_temp = (word1)[3], \ + + + +Eastlake & Hansen Informational [Page 53] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + (word1)[3] += (word2)[3], \ + ADDTO4_temp2 = (word1)[2], \ + (word1)[2] += (word2)[2] + ((word1)[3] < ADDTO4_temp), \ + ADDTO4_temp = (word1)[1], \ + (word1)[1] += (word2)[1] + ((word1)[2] < ADDTO4_temp2), \ + (word1)[0] += (word2)[0] + ((word1)[1] < ADDTO4_temp) ) + +/* + * Add the 2word value in word2 to word1. + */ +static uint32_t ADDTO2_temp; +#define SHA512_ADDTO2(word1, word2) ( \ + ADDTO2_temp = (word1)[1], \ + (word1)[1] += (word2)[1], \ + (word1)[0] += (word2)[0] + ((word1)[1] < ADDTO2_temp) ) + +/* + * SHA rotate ((word >> bits) | (word << (64-bits))) + */ +static uint32_t ROTR_temp1[2], ROTR_temp2[2]; +#define SHA512_ROTR(bits, word, ret) ( \ + SHA512_SHR((bits), (word), ROTR_temp1), \ + SHA512_SHL(64-(bits), (word), ROTR_temp2), \ + SHA512_OR(ROTR_temp1, ROTR_temp2, (ret)) ) + +/* + * Define the SHA SIGMA and sigma macros + * + * SHA512_ROTR(28,word) ^ SHA512_ROTR(34,word) ^ SHA512_ROTR(39,word) + */ +static uint32_t SIGMA0_temp1[2], SIGMA0_temp2[2], + SIGMA0_temp3[2], SIGMA0_temp4[2]; +#define SHA512_SIGMA0(word, ret) ( \ + SHA512_ROTR(28, (word), SIGMA0_temp1), \ + SHA512_ROTR(34, (word), SIGMA0_temp2), \ + SHA512_ROTR(39, (word), SIGMA0_temp3), \ + SHA512_XOR(SIGMA0_temp2, SIGMA0_temp3, SIGMA0_temp4), \ + SHA512_XOR(SIGMA0_temp1, SIGMA0_temp4, (ret)) ) + +/* + * SHA512_ROTR(14,word) ^ SHA512_ROTR(18,word) ^ SHA512_ROTR(41,word) + */ +static uint32_t SIGMA1_temp1[2], SIGMA1_temp2[2], + SIGMA1_temp3[2], SIGMA1_temp4[2]; +#define SHA512_SIGMA1(word, ret) ( \ + SHA512_ROTR(14, (word), SIGMA1_temp1), \ + SHA512_ROTR(18, (word), SIGMA1_temp2), \ + SHA512_ROTR(41, (word), SIGMA1_temp3), \ + + + +Eastlake & Hansen Informational [Page 54] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + SHA512_XOR(SIGMA1_temp2, SIGMA1_temp3, SIGMA1_temp4), \ + SHA512_XOR(SIGMA1_temp1, SIGMA1_temp4, (ret)) ) + +/* + * (SHA512_ROTR( 1,word) ^ SHA512_ROTR( 8,word) ^ SHA512_SHR( 7,word)) + */ +static uint32_t sigma0_temp1[2], sigma0_temp2[2], + sigma0_temp3[2], sigma0_temp4[2]; +#define SHA512_sigma0(word, ret) ( \ + SHA512_ROTR( 1, (word), sigma0_temp1), \ + SHA512_ROTR( 8, (word), sigma0_temp2), \ + SHA512_SHR( 7, (word), sigma0_temp3), \ + SHA512_XOR(sigma0_temp2, sigma0_temp3, sigma0_temp4), \ + SHA512_XOR(sigma0_temp1, sigma0_temp4, (ret)) ) + +/* + * (SHA512_ROTR(19,word) ^ SHA512_ROTR(61,word) ^ SHA512_SHR( 6,word)) + */ +static uint32_t sigma1_temp1[2], sigma1_temp2[2], + sigma1_temp3[2], sigma1_temp4[2]; +#define SHA512_sigma1(word, ret) ( \ + SHA512_ROTR(19, (word), sigma1_temp1), \ + SHA512_ROTR(61, (word), sigma1_temp2), \ + SHA512_SHR( 6, (word), sigma1_temp3), \ + SHA512_XOR(sigma1_temp2, sigma1_temp3, sigma1_temp4), \ + SHA512_XOR(sigma1_temp1, sigma1_temp4, (ret)) ) + +#ifndef USE_MODIFIED_MACROS +/* + * These definitions are the ones used in FIPS 180-3, section 4.1.3 + * Ch(x,y,z) ((x & y) ^ (~x & z)) + */ +static uint32_t Ch_temp1[2], Ch_temp2[2], Ch_temp3[2]; +#define SHA_Ch(x, y, z, ret) ( \ + SHA512_AND(x, y, Ch_temp1), \ + SHA512_TILDA(x, Ch_temp2), \ + SHA512_AND(Ch_temp2, z, Ch_temp3), \ + SHA512_XOR(Ch_temp1, Ch_temp3, (ret)) ) + +/* + * Maj(x,y,z) (((x)&(y)) ^ ((x)&(z)) ^ ((y)&(z))) + */ +static uint32_t Maj_temp1[2], Maj_temp2[2], + Maj_temp3[2], Maj_temp4[2]; +#define SHA_Maj(x, y, z, ret) ( \ + SHA512_AND(x, y, Maj_temp1), \ + SHA512_AND(x, z, Maj_temp2), \ + SHA512_AND(y, z, Maj_temp3), \ + + + +Eastlake & Hansen Informational [Page 55] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + SHA512_XOR(Maj_temp2, Maj_temp3, Maj_temp4), \ + SHA512_XOR(Maj_temp1, Maj_temp4, (ret)) ) +#else /* !USE_MODIFIED_MACROS */ +/* + * These definitions are potentially faster equivalents for the ones + * used in FIPS 180-3, section 4.1.3. + * ((x & y) ^ (~x & z)) becomes + * ((x & (y ^ z)) ^ z) + */ +#define SHA_Ch(x, y, z, ret) ( \ + (ret)[0] = (((x)[0] & ((y)[0] ^ (z)[0])) ^ (z)[0]), \ + (ret)[1] = (((x)[1] & ((y)[1] ^ (z)[1])) ^ (z)[1]) ) + +/* + * ((x & y) ^ (x & z) ^ (y & z)) becomes + * ((x & (y | z)) | (y & z)) + */ +#define SHA_Maj(x, y, z, ret) ( \ + ret[0] = (((x)[0] & ((y)[0] | (z)[0])) | ((y)[0] & (z)[0])), \ + ret[1] = (((x)[1] & ((y)[1] | (z)[1])) | ((y)[1] & (z)[1])) ) +#endif /* USE_MODIFIED_MACROS */ + +/* + * Add "length" to the length. + * Set Corrupted when overflow has occurred. + */ +static uint32_t addTemp[4] = { 0, 0, 0, 0 }; +#define SHA384_512AddLength(context, length) ( \ + addTemp[3] = (length), SHA512_ADDTO4((context)->Length, addTemp), \ + (context)->Corrupted = (((context)->Length[3] < (length)) && \ + ((context)->Length[2] == 0) && ((context)->Length[1] == 0) && \ + ((context)->Length[0] == 0)) ? shaInputTooLong : \ + (context)->Corrupted ) + +/* Local Function Prototypes */ +static int SHA384_512Reset(SHA512Context *context, + uint32_t H0[SHA512HashSize/4]); +static void SHA384_512ProcessMessageBlock(SHA512Context *context); +static void SHA384_512Finalize(SHA512Context *context, + uint8_t Pad_Byte); +static void SHA384_512PadMessage(SHA512Context *context, + uint8_t Pad_Byte); +static int SHA384_512ResultN( SHA512Context *context, + uint8_t Message_Digest[ ], int HashSize); + +/* Initial Hash Values: FIPS 180-3 sections 5.3.4 and 5.3.5 */ +static uint32_t SHA384_H0[SHA512HashSize/4] = { + 0xCBBB9D5D, 0xC1059ED8, 0x629A292A, 0x367CD507, 0x9159015A, + + + +Eastlake & Hansen Informational [Page 56] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + 0x3070DD17, 0x152FECD8, 0xF70E5939, 0x67332667, 0xFFC00B31, + 0x8EB44A87, 0x68581511, 0xDB0C2E0D, 0x64F98FA7, 0x47B5481D, + 0xBEFA4FA4 +}; +static uint32_t SHA512_H0[SHA512HashSize/4] = { + 0x6A09E667, 0xF3BCC908, 0xBB67AE85, 0x84CAA73B, 0x3C6EF372, + 0xFE94F82B, 0xA54FF53A, 0x5F1D36F1, 0x510E527F, 0xADE682D1, + 0x9B05688C, 0x2B3E6C1F, 0x1F83D9AB, 0xFB41BD6B, 0x5BE0CD19, + 0x137E2179 +}; + +#else /* !USE_32BIT_ONLY */ + +#include "sha-private.h" + +/* Define the SHA shift, rotate left and rotate right macros */ +#define SHA512_SHR(bits,word) (((uint64_t)(word)) >> (bits)) +#define SHA512_ROTR(bits,word) ((((uint64_t)(word)) >> (bits)) | \ + (((uint64_t)(word)) << (64-(bits)))) + +/* + * Define the SHA SIGMA and sigma macros + * + * SHA512_ROTR(28,word) ^ SHA512_ROTR(34,word) ^ SHA512_ROTR(39,word) + */ +#define SHA512_SIGMA0(word) \ + (SHA512_ROTR(28,word) ^ SHA512_ROTR(34,word) ^ SHA512_ROTR(39,word)) +#define SHA512_SIGMA1(word) \ + (SHA512_ROTR(14,word) ^ SHA512_ROTR(18,word) ^ SHA512_ROTR(41,word)) +#define SHA512_sigma0(word) \ + (SHA512_ROTR( 1,word) ^ SHA512_ROTR( 8,word) ^ SHA512_SHR( 7,word)) +#define SHA512_sigma1(word) \ + (SHA512_ROTR(19,word) ^ SHA512_ROTR(61,word) ^ SHA512_SHR( 6,word)) + +/* + * Add "length" to the length. + * Set Corrupted when overflow has occurred. + */ +static uint64_t addTemp; +#define SHA384_512AddLength(context, length) \ + (addTemp = context->Length_Low, context->Corrupted = \ + ((context->Length_Low += length) < addTemp) && \ + (++context->Length_High == 0) ? shaInputTooLong : \ + (context)->Corrupted) + +/* Local Function Prototypes */ +static int SHA384_512Reset(SHA512Context *context, + uint64_t H0[SHA512HashSize/8]); + + + +Eastlake & Hansen Informational [Page 57] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + +static void SHA384_512ProcessMessageBlock(SHA512Context *context); +static void SHA384_512Finalize(SHA512Context *context, + uint8_t Pad_Byte); +static void SHA384_512PadMessage(SHA512Context *context, + uint8_t Pad_Byte); +static int SHA384_512ResultN(SHA512Context *context, + uint8_t Message_Digest[ ], int HashSize); + +/* Initial Hash Values: FIPS 180-3 sections 5.3.4 and 5.3.5 */ +static uint64_t SHA384_H0[ ] = { + 0xCBBB9D5DC1059ED8ll, 0x629A292A367CD507ll, 0x9159015A3070DD17ll, + 0x152FECD8F70E5939ll, 0x67332667FFC00B31ll, 0x8EB44A8768581511ll, + 0xDB0C2E0D64F98FA7ll, 0x47B5481DBEFA4FA4ll +}; +static uint64_t SHA512_H0[ ] = { + 0x6A09E667F3BCC908ll, 0xBB67AE8584CAA73Bll, 0x3C6EF372FE94F82Bll, + 0xA54FF53A5F1D36F1ll, 0x510E527FADE682D1ll, 0x9B05688C2B3E6C1Fll, + 0x1F83D9ABFB41BD6Bll, 0x5BE0CD19137E2179ll +}; + +#endif /* USE_32BIT_ONLY */ + +/* + * SHA384Reset + * + * Description: + * This function will initialize the SHA384Context in preparation + * for computing a new SHA384 message digest. + * + * Parameters: + * context: [in/out] + * The context to reset. + * + * Returns: + * sha Error Code. + * + */ +int SHA384Reset(SHA384Context *context) +{ + return SHA384_512Reset(context, SHA384_H0); +} + +/* + * SHA384Input + * + * Description: + * This function accepts an array of octets as the next portion + * of the message. + + + +Eastlake & Hansen Informational [Page 58] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + * + * Parameters: + * context: [in/out] + * The SHA context to update. + * message_array[ ]: [in] + * An array of octets representing the next portion of + * the message. + * length: [in] + * The length of the message in message_array. + * + * Returns: + * sha Error Code. + * + */ +int SHA384Input(SHA384Context *context, + const uint8_t *message_array, unsigned int length) +{ + return SHA512Input(context, message_array, length); +} + +/* + * SHA384FinalBits + * + * Description: + * This function will add in any final bits of the message. + * + * Parameters: + * context: [in/out] + * The SHA context to update. + * message_bits: [in] + * The final bits of the message, in the upper portion of the + * byte. (Use 0b###00000 instead of 0b00000### to input the + * three bits ###.) + * length: [in] + * The number of bits in message_bits, between 1 and 7. + * + * Returns: + * sha Error Code. + * + */ +int SHA384FinalBits(SHA384Context *context, + uint8_t message_bits, unsigned int length) +{ + return SHA512FinalBits(context, message_bits, length); +} + +/* + * SHA384Result + + + +Eastlake & Hansen Informational [Page 59] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + * + * Description: + * This function will return the 384-bit message digest + * into the Message_Digest array provided by the caller. + * NOTE: + * The first octet of hash is stored in the element with index 0, + * the last octet of hash in the element with index 47. + * + * Parameters: + * context: [in/out] + * The context to use to calculate the SHA hash. + * Message_Digest[ ]: [out] + * Where the digest is returned. + * + * Returns: + * sha Error Code. + * + */ +int SHA384Result(SHA384Context *context, + uint8_t Message_Digest[SHA384HashSize]) +{ + return SHA384_512ResultN(context, Message_Digest, SHA384HashSize); +} + +/* + * SHA512Reset + * + * Description: + * This function will initialize the SHA512Context in preparation + * for computing a new SHA512 message digest. + * + * Parameters: + * context: [in/out] + * The context to reset. + * + * Returns: + * sha Error Code. + * + */ +int SHA512Reset(SHA512Context *context) +{ + return SHA384_512Reset(context, SHA512_H0); +} + +/* + * SHA512Input + * + * Description: + + + +Eastlake & Hansen Informational [Page 60] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + * This function accepts an array of octets as the next portion + * of the message. + * + * Parameters: + * context: [in/out] + * The SHA context to update. + * message_array[ ]: [in] + * An array of octets representing the next portion of + * the message. + * length: [in] + * The length of the message in message_array. + * + * Returns: + * sha Error Code. + * + */ +int SHA512Input(SHA512Context *context, + const uint8_t *message_array, + unsigned int length) +{ + if (!context) return shaNull; + if (!length) return shaSuccess; + if (!message_array) return shaNull; + if (context->Computed) return context->Corrupted = shaStateError; + if (context->Corrupted) return context->Corrupted; + + while (length--) { + context->Message_Block[context->Message_Block_Index++] = + *message_array; + + if ((SHA384_512AddLength(context, 8) == shaSuccess) && + (context->Message_Block_Index == SHA512_Message_Block_Size)) + SHA384_512ProcessMessageBlock(context); + + message_array++; + } + + return context->Corrupted; +} + +/* + * SHA512FinalBits + * + * Description: + * This function will add in any final bits of the message. + * + * Parameters: + * context: [in/out] + + + +Eastlake & Hansen Informational [Page 61] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + * The SHA context to update. + * message_bits: [in] + * The final bits of the message, in the upper portion of the + * byte. (Use 0b###00000 instead of 0b00000### to input the + * three bits ###.) + * length: [in] + * The number of bits in message_bits, between 1 and 7. + * + * Returns: + * sha Error Code. + * + */ +int SHA512FinalBits(SHA512Context *context, + uint8_t message_bits, unsigned int length) +{ + static uint8_t masks[8] = { + /* 0 0b00000000 */ 0x00, /* 1 0b10000000 */ 0x80, + /* 2 0b11000000 */ 0xC0, /* 3 0b11100000 */ 0xE0, + /* 4 0b11110000 */ 0xF0, /* 5 0b11111000 */ 0xF8, + /* 6 0b11111100 */ 0xFC, /* 7 0b11111110 */ 0xFE + }; + static uint8_t markbit[8] = { + /* 0 0b10000000 */ 0x80, /* 1 0b01000000 */ 0x40, + /* 2 0b00100000 */ 0x20, /* 3 0b00010000 */ 0x10, + /* 4 0b00001000 */ 0x08, /* 5 0b00000100 */ 0x04, + /* 6 0b00000010 */ 0x02, /* 7 0b00000001 */ 0x01 + }; + + if (!context) return shaNull; + if (!length) return shaSuccess; + if (context->Corrupted) return context->Corrupted; + if (context->Computed) return context->Corrupted = shaStateError; + if (length >= 8) return context->Corrupted = shaBadParam; + + SHA384_512AddLength(context, length); + SHA384_512Finalize(context, (uint8_t) + ((message_bits & masks[length]) | markbit[length])); + + return context->Corrupted; +} + +/* + * SHA512Result + * + * Description: + * This function will return the 512-bit message digest + * into the Message_Digest array provided by the caller. + * NOTE: + + + +Eastlake & Hansen Informational [Page 62] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + * The first octet of hash is stored in the element with index 0, + * the last octet of hash in the element with index 63. + * + * Parameters: + * context: [in/out] + * The context to use to calculate the SHA hash. + * Message_Digest[ ]: [out] + * Where the digest is returned. + * + * Returns: + * sha Error Code. + * + */ +int SHA512Result(SHA512Context *context, + uint8_t Message_Digest[SHA512HashSize]) +{ + return SHA384_512ResultN(context, Message_Digest, SHA512HashSize); +} + +/* + * SHA384_512Reset + * + * Description: + * This helper function will initialize the SHA512Context in + * preparation for computing a new SHA384 or SHA512 message + * digest. + * + * Parameters: + * context: [in/out] + * The context to reset. + * H0[ ]: [in] + * The initial hash value array to use. + * + * Returns: + * sha Error Code. + * + */ +#ifdef USE_32BIT_ONLY +static int SHA384_512Reset(SHA512Context *context, + uint32_t H0[SHA512HashSize/4]) +#else /* !USE_32BIT_ONLY */ +static int SHA384_512Reset(SHA512Context *context, + uint64_t H0[SHA512HashSize/8]) +#endif /* USE_32BIT_ONLY */ +{ + int i; + if (!context) return shaNull; + + + + +Eastlake & Hansen Informational [Page 63] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + context->Message_Block_Index = 0; + +#ifdef USE_32BIT_ONLY + context->Length[0] = context->Length[1] = + context->Length[2] = context->Length[3] = 0; + + for (i = 0; i < SHA512HashSize/4; i++) + context->Intermediate_Hash[i] = H0[i]; +#else /* !USE_32BIT_ONLY */ + context->Length_High = context->Length_Low = 0; + + for (i = 0; i < SHA512HashSize/8; i++) + context->Intermediate_Hash[i] = H0[i]; +#endif /* USE_32BIT_ONLY */ + + context->Computed = 0; + context->Corrupted = shaSuccess; + + return shaSuccess; +} + +/* + * SHA384_512ProcessMessageBlock + * + * Description: + * This helper function will process the next 1024 bits of the + * message stored in the Message_Block array. + * + * Parameters: + * context: [in/out] + * The SHA context to update. + * + * Returns: + * Nothing. + * + * Comments: + * Many of the variable names in this code, especially the + * single character names, were used because those were the + * names used in the Secure Hash Standard. + * + * + */ +static void SHA384_512ProcessMessageBlock(SHA512Context *context) +{ +#ifdef USE_32BIT_ONLY + /* Constants defined in FIPS 180-3, section 4.2.3 */ + static const uint32_t K[80*2] = { + 0x428A2F98, 0xD728AE22, 0x71374491, 0x23EF65CD, 0xB5C0FBCF, + + + +Eastlake & Hansen Informational [Page 64] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + 0xEC4D3B2F, 0xE9B5DBA5, 0x8189DBBC, 0x3956C25B, 0xF348B538, + 0x59F111F1, 0xB605D019, 0x923F82A4, 0xAF194F9B, 0xAB1C5ED5, + 0xDA6D8118, 0xD807AA98, 0xA3030242, 0x12835B01, 0x45706FBE, + 0x243185BE, 0x4EE4B28C, 0x550C7DC3, 0xD5FFB4E2, 0x72BE5D74, + 0xF27B896F, 0x80DEB1FE, 0x3B1696B1, 0x9BDC06A7, 0x25C71235, + 0xC19BF174, 0xCF692694, 0xE49B69C1, 0x9EF14AD2, 0xEFBE4786, + 0x384F25E3, 0x0FC19DC6, 0x8B8CD5B5, 0x240CA1CC, 0x77AC9C65, + 0x2DE92C6F, 0x592B0275, 0x4A7484AA, 0x6EA6E483, 0x5CB0A9DC, + 0xBD41FBD4, 0x76F988DA, 0x831153B5, 0x983E5152, 0xEE66DFAB, + 0xA831C66D, 0x2DB43210, 0xB00327C8, 0x98FB213F, 0xBF597FC7, + 0xBEEF0EE4, 0xC6E00BF3, 0x3DA88FC2, 0xD5A79147, 0x930AA725, + 0x06CA6351, 0xE003826F, 0x14292967, 0x0A0E6E70, 0x27B70A85, + 0x46D22FFC, 0x2E1B2138, 0x5C26C926, 0x4D2C6DFC, 0x5AC42AED, + 0x53380D13, 0x9D95B3DF, 0x650A7354, 0x8BAF63DE, 0x766A0ABB, + 0x3C77B2A8, 0x81C2C92E, 0x47EDAEE6, 0x92722C85, 0x1482353B, + 0xA2BFE8A1, 0x4CF10364, 0xA81A664B, 0xBC423001, 0xC24B8B70, + 0xD0F89791, 0xC76C51A3, 0x0654BE30, 0xD192E819, 0xD6EF5218, + 0xD6990624, 0x5565A910, 0xF40E3585, 0x5771202A, 0x106AA070, + 0x32BBD1B8, 0x19A4C116, 0xB8D2D0C8, 0x1E376C08, 0x5141AB53, + 0x2748774C, 0xDF8EEB99, 0x34B0BCB5, 0xE19B48A8, 0x391C0CB3, + 0xC5C95A63, 0x4ED8AA4A, 0xE3418ACB, 0x5B9CCA4F, 0x7763E373, + 0x682E6FF3, 0xD6B2B8A3, 0x748F82EE, 0x5DEFB2FC, 0x78A5636F, + 0x43172F60, 0x84C87814, 0xA1F0AB72, 0x8CC70208, 0x1A6439EC, + 0x90BEFFFA, 0x23631E28, 0xA4506CEB, 0xDE82BDE9, 0xBEF9A3F7, + 0xB2C67915, 0xC67178F2, 0xE372532B, 0xCA273ECE, 0xEA26619C, + 0xD186B8C7, 0x21C0C207, 0xEADA7DD6, 0xCDE0EB1E, 0xF57D4F7F, + 0xEE6ED178, 0x06F067AA, 0x72176FBA, 0x0A637DC5, 0xA2C898A6, + 0x113F9804, 0xBEF90DAE, 0x1B710B35, 0x131C471B, 0x28DB77F5, + 0x23047D84, 0x32CAAB7B, 0x40C72493, 0x3C9EBE0A, 0x15C9BEBC, + 0x431D67C4, 0x9C100D4C, 0x4CC5D4BE, 0xCB3E42B6, 0x597F299C, + 0xFC657E2A, 0x5FCB6FAB, 0x3AD6FAEC, 0x6C44198C, 0x4A475817 + }; + int t, t2, t8; /* Loop counter */ + uint32_t temp1[2], temp2[2], /* Temporary word values */ + temp3[2], temp4[2], temp5[2]; + uint32_t W[2*80]; /* Word sequence */ + uint32_t A[2], B[2], C[2], D[2], /* Word buffers */ + E[2], F[2], G[2], H[2]; + + /* Initialize the first 16 words in the array W */ + for (t = t2 = t8 = 0; t < 16; t++, t8 += 8) { + W[t2++] = ((((uint32_t)context->Message_Block[t8 ])) << 24) | + ((((uint32_t)context->Message_Block[t8 + 1])) << 16) | + ((((uint32_t)context->Message_Block[t8 + 2])) << 8) | + ((((uint32_t)context->Message_Block[t8 + 3]))); + W[t2++] = ((((uint32_t)context->Message_Block[t8 + 4])) << 24) | + ((((uint32_t)context->Message_Block[t8 + 5])) << 16) | + ((((uint32_t)context->Message_Block[t8 + 6])) << 8) | + + + +Eastlake & Hansen Informational [Page 65] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + ((((uint32_t)context->Message_Block[t8 + 7]))); + } + + for (t = 16; t < 80; t++, t2 += 2) { + /* W[t] = SHA512_sigma1(W[t-2]) + W[t-7] + + SHA512_sigma0(W[t-15]) + W[t-16]; */ + uint32_t *Wt2 = &W[t2-2*2]; + uint32_t *Wt7 = &W[t2-7*2]; + uint32_t *Wt15 = &W[t2-15*2]; + uint32_t *Wt16 = &W[t2-16*2]; + SHA512_sigma1(Wt2, temp1); + SHA512_ADD(temp1, Wt7, temp2); + SHA512_sigma0(Wt15, temp1); + SHA512_ADD(temp1, Wt16, temp3); + SHA512_ADD(temp2, temp3, &W[t2]); + } + + A[0] = context->Intermediate_Hash[0]; + A[1] = context->Intermediate_Hash[1]; + B[0] = context->Intermediate_Hash[2]; + B[1] = context->Intermediate_Hash[3]; + C[0] = context->Intermediate_Hash[4]; + C[1] = context->Intermediate_Hash[5]; + D[0] = context->Intermediate_Hash[6]; + D[1] = context->Intermediate_Hash[7]; + E[0] = context->Intermediate_Hash[8]; + E[1] = context->Intermediate_Hash[9]; + F[0] = context->Intermediate_Hash[10]; + F[1] = context->Intermediate_Hash[11]; + G[0] = context->Intermediate_Hash[12]; + G[1] = context->Intermediate_Hash[13]; + H[0] = context->Intermediate_Hash[14]; + H[1] = context->Intermediate_Hash[15]; + + for (t = t2 = 0; t < 80; t++, t2 += 2) { + /* + * temp1 = H + SHA512_SIGMA1(E) + SHA_Ch(E,F,G) + K[t] + W[t]; + */ + SHA512_SIGMA1(E,temp1); + SHA512_ADD(H, temp1, temp2); + SHA_Ch(E,F,G,temp3); + SHA512_ADD(temp2, temp3, temp4); + SHA512_ADD(&K[t2], &W[t2], temp5); + SHA512_ADD(temp4, temp5, temp1); + /* + * temp2 = SHA512_SIGMA0(A) + SHA_Maj(A,B,C); + */ + SHA512_SIGMA0(A,temp3); + + + +Eastlake & Hansen Informational [Page 66] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + SHA_Maj(A,B,C,temp4); + SHA512_ADD(temp3, temp4, temp2); + H[0] = G[0]; H[1] = G[1]; + G[0] = F[0]; G[1] = F[1]; + F[0] = E[0]; F[1] = E[1]; + SHA512_ADD(D, temp1, E); + D[0] = C[0]; D[1] = C[1]; + C[0] = B[0]; C[1] = B[1]; + B[0] = A[0]; B[1] = A[1]; + SHA512_ADD(temp1, temp2, A); + } + + SHA512_ADDTO2(&context->Intermediate_Hash[0], A); + SHA512_ADDTO2(&context->Intermediate_Hash[2], B); + SHA512_ADDTO2(&context->Intermediate_Hash[4], C); + SHA512_ADDTO2(&context->Intermediate_Hash[6], D); + SHA512_ADDTO2(&context->Intermediate_Hash[8], E); + SHA512_ADDTO2(&context->Intermediate_Hash[10], F); + SHA512_ADDTO2(&context->Intermediate_Hash[12], G); + SHA512_ADDTO2(&context->Intermediate_Hash[14], H); + +#else /* !USE_32BIT_ONLY */ + /* Constants defined in FIPS 180-3, section 4.2.3 */ + static const uint64_t K[80] = { + 0x428A2F98D728AE22ll, 0x7137449123EF65CDll, 0xB5C0FBCFEC4D3B2Fll, + 0xE9B5DBA58189DBBCll, 0x3956C25BF348B538ll, 0x59F111F1B605D019ll, + 0x923F82A4AF194F9Bll, 0xAB1C5ED5DA6D8118ll, 0xD807AA98A3030242ll, + 0x12835B0145706FBEll, 0x243185BE4EE4B28Cll, 0x550C7DC3D5FFB4E2ll, + 0x72BE5D74F27B896Fll, 0x80DEB1FE3B1696B1ll, 0x9BDC06A725C71235ll, + 0xC19BF174CF692694ll, 0xE49B69C19EF14AD2ll, 0xEFBE4786384F25E3ll, + 0x0FC19DC68B8CD5B5ll, 0x240CA1CC77AC9C65ll, 0x2DE92C6F592B0275ll, + 0x4A7484AA6EA6E483ll, 0x5CB0A9DCBD41FBD4ll, 0x76F988DA831153B5ll, + 0x983E5152EE66DFABll, 0xA831C66D2DB43210ll, 0xB00327C898FB213Fll, + 0xBF597FC7BEEF0EE4ll, 0xC6E00BF33DA88FC2ll, 0xD5A79147930AA725ll, + 0x06CA6351E003826Fll, 0x142929670A0E6E70ll, 0x27B70A8546D22FFCll, + 0x2E1B21385C26C926ll, 0x4D2C6DFC5AC42AEDll, 0x53380D139D95B3DFll, + 0x650A73548BAF63DEll, 0x766A0ABB3C77B2A8ll, 0x81C2C92E47EDAEE6ll, + 0x92722C851482353Bll, 0xA2BFE8A14CF10364ll, 0xA81A664BBC423001ll, + 0xC24B8B70D0F89791ll, 0xC76C51A30654BE30ll, 0xD192E819D6EF5218ll, + 0xD69906245565A910ll, 0xF40E35855771202All, 0x106AA07032BBD1B8ll, + 0x19A4C116B8D2D0C8ll, 0x1E376C085141AB53ll, 0x2748774CDF8EEB99ll, + 0x34B0BCB5E19B48A8ll, 0x391C0CB3C5C95A63ll, 0x4ED8AA4AE3418ACBll, + 0x5B9CCA4F7763E373ll, 0x682E6FF3D6B2B8A3ll, 0x748F82EE5DEFB2FCll, + 0x78A5636F43172F60ll, 0x84C87814A1F0AB72ll, 0x8CC702081A6439ECll, + 0x90BEFFFA23631E28ll, 0xA4506CEBDE82BDE9ll, 0xBEF9A3F7B2C67915ll, + 0xC67178F2E372532Bll, 0xCA273ECEEA26619Cll, 0xD186B8C721C0C207ll, + 0xEADA7DD6CDE0EB1Ell, 0xF57D4F7FEE6ED178ll, 0x06F067AA72176FBAll, + 0x0A637DC5A2C898A6ll, 0x113F9804BEF90DAEll, 0x1B710B35131C471Bll, + + + +Eastlake & Hansen Informational [Page 67] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + 0x28DB77F523047D84ll, 0x32CAAB7B40C72493ll, 0x3C9EBE0A15C9BEBCll, + 0x431D67C49C100D4Cll, 0x4CC5D4BECB3E42B6ll, 0x597F299CFC657E2All, + 0x5FCB6FAB3AD6FAECll, 0x6C44198C4A475817ll + }; + int t, t8; /* Loop counter */ + uint64_t temp1, temp2; /* Temporary word value */ + uint64_t W[80]; /* Word sequence */ + uint64_t A, B, C, D, E, F, G, H; /* Word buffers */ + + /* + * Initialize the first 16 words in the array W + */ + for (t = t8 = 0; t < 16; t++, t8 += 8) + W[t] = ((uint64_t)(context->Message_Block[t8 ]) << 56) | + ((uint64_t)(context->Message_Block[t8 + 1]) << 48) | + ((uint64_t)(context->Message_Block[t8 + 2]) << 40) | + ((uint64_t)(context->Message_Block[t8 + 3]) << 32) | + ((uint64_t)(context->Message_Block[t8 + 4]) << 24) | + ((uint64_t)(context->Message_Block[t8 + 5]) << 16) | + ((uint64_t)(context->Message_Block[t8 + 6]) << 8) | + ((uint64_t)(context->Message_Block[t8 + 7])); + + for (t = 16; t < 80; t++) + W[t] = SHA512_sigma1(W[t-2]) + W[t-7] + + SHA512_sigma0(W[t-15]) + W[t-16]; + A = context->Intermediate_Hash[0]; + B = context->Intermediate_Hash[1]; + C = context->Intermediate_Hash[2]; + D = context->Intermediate_Hash[3]; + E = context->Intermediate_Hash[4]; + F = context->Intermediate_Hash[5]; + G = context->Intermediate_Hash[6]; + H = context->Intermediate_Hash[7]; + + for (t = 0; t < 80; t++) { + temp1 = H + SHA512_SIGMA1(E) + SHA_Ch(E,F,G) + K[t] + W[t]; + temp2 = SHA512_SIGMA0(A) + SHA_Maj(A,B,C); + H = G; + G = F; + F = E; + E = D + temp1; + D = C; + C = B; + B = A; + A = temp1 + temp2; + } + + context->Intermediate_Hash[0] += A; + + + +Eastlake & Hansen Informational [Page 68] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + context->Intermediate_Hash[1] += B; + context->Intermediate_Hash[2] += C; + context->Intermediate_Hash[3] += D; + context->Intermediate_Hash[4] += E; + context->Intermediate_Hash[5] += F; + context->Intermediate_Hash[6] += G; + context->Intermediate_Hash[7] += H; +#endif /* USE_32BIT_ONLY */ + + context->Message_Block_Index = 0; +} + +/* + * SHA384_512Finalize + * + * Description: + * This helper function finishes off the digest calculations. + * + * Parameters: + * context: [in/out] + * The SHA context to update. + * Pad_Byte: [in] + * The last byte to add to the message block before the 0-padding + * and length. This will contain the last bits of the message + * followed by another single bit. If the message was an + * exact multiple of 8-bits long, Pad_Byte will be 0x80. + * + * Returns: + * sha Error Code. + * + */ +static void SHA384_512Finalize(SHA512Context *context, + uint8_t Pad_Byte) +{ + int_least16_t i; + SHA384_512PadMessage(context, Pad_Byte); + /* message may be sensitive, clear it out */ + for (i = 0; i < SHA512_Message_Block_Size; ++i) + context->Message_Block[i] = 0; +#ifdef USE_32BIT_ONLY /* and clear length */ + context->Length[0] = context->Length[1] = 0; + context->Length[2] = context->Length[3] = 0; +#else /* !USE_32BIT_ONLY */ + context->Length_High = context->Length_Low = 0; +#endif /* USE_32BIT_ONLY */ + context->Computed = 1; +} + + + + +Eastlake & Hansen Informational [Page 69] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + +/* + * SHA384_512PadMessage + * + * Description: + * According to the standard, the message must be padded to the next + * even multiple of 1024 bits. The first padding bit must be a '1'. + * The last 128 bits represent the length of the original message. + * All bits in between should be 0. This helper function will + * pad the message according to those rules by filling the + * Message_Block array accordingly. When it returns, it can be + * assumed that the message digest has been computed. + * + * Parameters: + * context: [in/out] + * The context to pad. + * Pad_Byte: [in] + * The last byte to add to the message block before the 0-padding + * and length. This will contain the last bits of the message + * followed by another single bit. If the message was an + * exact multiple of 8-bits long, Pad_Byte will be 0x80. + * + * Returns: + * Nothing. + * + */ +static void SHA384_512PadMessage(SHA512Context *context, + uint8_t Pad_Byte) +{ + /* + * Check to see if the current message block is too small to hold + * the initial padding bits and length. If so, we will pad the + * block, process it, and then continue padding into a second + * block. + */ + if (context->Message_Block_Index >= (SHA512_Message_Block_Size-16)) { + context->Message_Block[context->Message_Block_Index++] = Pad_Byte; + while (context->Message_Block_Index < SHA512_Message_Block_Size) + context->Message_Block[context->Message_Block_Index++] = 0; + + SHA384_512ProcessMessageBlock(context); + } else + context->Message_Block[context->Message_Block_Index++] = Pad_Byte; + + while (context->Message_Block_Index < (SHA512_Message_Block_Size-16)) + context->Message_Block[context->Message_Block_Index++] = 0; + + /* + * Store the message length as the last 16 octets + + + +Eastlake & Hansen Informational [Page 70] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + */ +#ifdef USE_32BIT_ONLY + context->Message_Block[112] = (uint8_t)(context->Length[0] >> 24); + context->Message_Block[113] = (uint8_t)(context->Length[0] >> 16); + context->Message_Block[114] = (uint8_t)(context->Length[0] >> 8); + context->Message_Block[115] = (uint8_t)(context->Length[0]); + context->Message_Block[116] = (uint8_t)(context->Length[1] >> 24); + context->Message_Block[117] = (uint8_t)(context->Length[1] >> 16); + context->Message_Block[118] = (uint8_t)(context->Length[1] >> 8); + context->Message_Block[119] = (uint8_t)(context->Length[1]); + + context->Message_Block[120] = (uint8_t)(context->Length[2] >> 24); + context->Message_Block[121] = (uint8_t)(context->Length[2] >> 16); + context->Message_Block[122] = (uint8_t)(context->Length[2] >> 8); + context->Message_Block[123] = (uint8_t)(context->Length[2]); + context->Message_Block[124] = (uint8_t)(context->Length[3] >> 24); + context->Message_Block[125] = (uint8_t)(context->Length[3] >> 16); + context->Message_Block[126] = (uint8_t)(context->Length[3] >> 8); + context->Message_Block[127] = (uint8_t)(context->Length[3]); +#else /* !USE_32BIT_ONLY */ + context->Message_Block[112] = (uint8_t)(context->Length_High >> 56); + context->Message_Block[113] = (uint8_t)(context->Length_High >> 48); + context->Message_Block[114] = (uint8_t)(context->Length_High >> 40); + context->Message_Block[115] = (uint8_t)(context->Length_High >> 32); + context->Message_Block[116] = (uint8_t)(context->Length_High >> 24); + context->Message_Block[117] = (uint8_t)(context->Length_High >> 16); + context->Message_Block[118] = (uint8_t)(context->Length_High >> 8); + context->Message_Block[119] = (uint8_t)(context->Length_High); + + context->Message_Block[120] = (uint8_t)(context->Length_Low >> 56); + context->Message_Block[121] = (uint8_t)(context->Length_Low >> 48); + context->Message_Block[122] = (uint8_t)(context->Length_Low >> 40); + context->Message_Block[123] = (uint8_t)(context->Length_Low >> 32); + context->Message_Block[124] = (uint8_t)(context->Length_Low >> 24); + context->Message_Block[125] = (uint8_t)(context->Length_Low >> 16); + context->Message_Block[126] = (uint8_t)(context->Length_Low >> 8); + context->Message_Block[127] = (uint8_t)(context->Length_Low); +#endif /* USE_32BIT_ONLY */ + + SHA384_512ProcessMessageBlock(context); +} + +/* + * SHA384_512ResultN + * + * Description: + * This helper function will return the 384-bit or 512-bit message + * digest into the Message_Digest array provided by the caller. + + + +Eastlake & Hansen Informational [Page 71] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + * NOTE: + * The first octet of hash is stored in the element with index 0, + * the last octet of hash in the element with index 47/63. + * + * Parameters: + * context: [in/out] + * The context to use to calculate the SHA hash. + * Message_Digest[ ]: [out] + * Where the digest is returned. + * HashSize: [in] + * The size of the hash, either 48 or 64. + * + * Returns: + * sha Error Code. + * + */ +static int SHA384_512ResultN(SHA512Context *context, + uint8_t Message_Digest[ ], int HashSize) +{ + int i; +#ifdef USE_32BIT_ONLY + int i2; +#endif /* USE_32BIT_ONLY */ + + if (!context) return shaNull; + if (!Message_Digest) return shaNull; + if (context->Corrupted) return context->Corrupted; + + if (!context->Computed) + SHA384_512Finalize(context, 0x80); + +#ifdef USE_32BIT_ONLY + for (i = i2 = 0; i < HashSize; ) { + Message_Digest[i++]=(uint8_t)(context->Intermediate_Hash[i2]>>24); + Message_Digest[i++]=(uint8_t)(context->Intermediate_Hash[i2]>>16); + Message_Digest[i++]=(uint8_t)(context->Intermediate_Hash[i2]>>8); + Message_Digest[i++]=(uint8_t)(context->Intermediate_Hash[i2++]); + Message_Digest[i++]=(uint8_t)(context->Intermediate_Hash[i2]>>24); + Message_Digest[i++]=(uint8_t)(context->Intermediate_Hash[i2]>>16); + Message_Digest[i++]=(uint8_t)(context->Intermediate_Hash[i2]>>8); + Message_Digest[i++]=(uint8_t)(context->Intermediate_Hash[i2++]); + } +#else /* !USE_32BIT_ONLY */ + for (i = 0; i < HashSize; ++i) + Message_Digest[i] = (uint8_t) + (context->Intermediate_Hash[i>>3] >> 8 * ( 7 - ( i % 8 ) )); +#endif /* USE_32BIT_ONLY */ + + + + +Eastlake & Hansen Informational [Page 72] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + return shaSuccess; +} + +8.2.4. usha.c + +/**************************** usha.c ***************************/ +/***************** See RFC 6234 for details. *******************/ +/* Copyright (c) 2011 IETF Trust and the persons identified as */ +/* authors of the code. All rights reserved. */ +/* See sha.h for terms of use and redistribution. */ + +/* + * Description: + * This file implements a unified interface to the SHA algorithms. + */ + +#include "sha.h" + +/* + * USHAReset + * + * Description: + * This function will initialize the SHA Context in preparation + * for computing a new SHA message digest. + * + * Parameters: + * context: [in/out] + * The context to reset. + * whichSha: [in] + * Selects which SHA reset to call + * + * Returns: + * sha Error Code. + * + */ +int USHAReset(USHAContext *context, enum SHAversion whichSha) +{ + if (!context) return shaNull; + context->whichSha = whichSha; + switch (whichSha) { + case SHA1: return SHA1Reset((SHA1Context*)&context->ctx); + case SHA224: return SHA224Reset((SHA224Context*)&context->ctx); + case SHA256: return SHA256Reset((SHA256Context*)&context->ctx); + case SHA384: return SHA384Reset((SHA384Context*)&context->ctx); + case SHA512: return SHA512Reset((SHA512Context*)&context->ctx); + default: return shaBadParam; + } +} + + + +Eastlake & Hansen Informational [Page 73] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + +/* + * USHAInput + * + * Description: + * This function accepts an array of octets as the next portion + * of the message. + * + * Parameters: + * context: [in/out] + * The SHA context to update. + * message_array: [in] + * An array of octets representing the next portion of + * the message. + * length: [in] + * The length of the message in message_array. + * + * Returns: + * sha Error Code. + * + */ +int USHAInput(USHAContext *context, + const uint8_t *bytes, unsigned int bytecount) +{ + if (!context) return shaNull; + switch (context->whichSha) { + case SHA1: + return SHA1Input((SHA1Context*)&context->ctx, bytes, + bytecount); + case SHA224: + return SHA224Input((SHA224Context*)&context->ctx, bytes, + bytecount); + case SHA256: + return SHA256Input((SHA256Context*)&context->ctx, bytes, + bytecount); + case SHA384: + return SHA384Input((SHA384Context*)&context->ctx, bytes, + bytecount); + case SHA512: + return SHA512Input((SHA512Context*)&context->ctx, bytes, + bytecount); + default: return shaBadParam; + } +} + + + + + + + + +Eastlake & Hansen Informational [Page 74] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + +/* + * USHAFinalBits + * + * Description: + * This function will add in any final bits of the message. + * + * Parameters: + * context: [in/out] + * The SHA context to update. + * message_bits: [in] + * The final bits of the message, in the upper portion of the + * byte. (Use 0b###00000 instead of 0b00000### to input the + * three bits ###.) + * length: [in] + * The number of bits in message_bits, between 1 and 7. + * + * Returns: + * sha Error Code. + */ +int USHAFinalBits(USHAContext *context, + uint8_t bits, unsigned int bit_count) +{ + if (!context) return shaNull; + switch (context->whichSha) { + case SHA1: + return SHA1FinalBits((SHA1Context*)&context->ctx, bits, + bit_count); + case SHA224: + return SHA224FinalBits((SHA224Context*)&context->ctx, bits, + bit_count); + case SHA256: + return SHA256FinalBits((SHA256Context*)&context->ctx, bits, + bit_count); + case SHA384: + return SHA384FinalBits((SHA384Context*)&context->ctx, bits, + bit_count); + case SHA512: + return SHA512FinalBits((SHA512Context*)&context->ctx, bits, + bit_count); + default: return shaBadParam; + } +} + + + + + + + + + +Eastlake & Hansen Informational [Page 75] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + +/* + * USHAResult + * + * Description: + * This function will return the message digest of the appropriate + * bit size, as returned by USHAHashSizeBits(whichSHA) for the + * 'whichSHA' value used in the preceeding call to USHAReset, + * into the Message_Digest array provided by the caller. + * + * Parameters: + * context: [in/out] + * The context to use to calculate the SHA-1 hash. + * Message_Digest: [out] + * Where the digest is returned. + * + * Returns: + * sha Error Code. + * + */ +int USHAResult(USHAContext *context, + uint8_t Message_Digest[USHAMaxHashSize]) +{ + if (!context) return shaNull; + switch (context->whichSha) { + case SHA1: + return SHA1Result((SHA1Context*)&context->ctx, Message_Digest); + case SHA224: + return SHA224Result((SHA224Context*)&context->ctx, + Message_Digest); + case SHA256: + return SHA256Result((SHA256Context*)&context->ctx, + Message_Digest); + case SHA384: + return SHA384Result((SHA384Context*)&context->ctx, + Message_Digest); + case SHA512: + return SHA512Result((SHA512Context*)&context->ctx, + Message_Digest); + default: return shaBadParam; + } +} + +/* + * USHABlockSize + * + * Description: + * This function will return the blocksize for the given SHA + * algorithm. + + + +Eastlake & Hansen Informational [Page 76] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + * + * Parameters: + * whichSha: + * which SHA algorithm to query + * + * Returns: + * block size + * + */ +int USHABlockSize(enum SHAversion whichSha) +{ + switch (whichSha) { + case SHA1: return SHA1_Message_Block_Size; + case SHA224: return SHA224_Message_Block_Size; + case SHA256: return SHA256_Message_Block_Size; + case SHA384: return SHA384_Message_Block_Size; + default: + case SHA512: return SHA512_Message_Block_Size; + } +} + +/* + * USHAHashSize + * + * Description: + * This function will return the hashsize for the given SHA + * algorithm. + * + * Parameters: + * whichSha: + * which SHA algorithm to query + * + * Returns: + * hash size + * + */ +int USHAHashSize(enum SHAversion whichSha) +{ + switch (whichSha) { + case SHA1: return SHA1HashSize; + case SHA224: return SHA224HashSize; + case SHA256: return SHA256HashSize; + case SHA384: return SHA384HashSize; + default: + case SHA512: return SHA512HashSize; + } +} + + + + +Eastlake & Hansen Informational [Page 77] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + +/* + * USHAHashSizeBits + * + * Description: + * This function will return the hashsize for the given SHA + * algorithm, expressed in bits. + * + * Parameters: + * whichSha: + * which SHA algorithm to query + * + * Returns: + * hash size in bits + * + */ +int USHAHashSizeBits(enum SHAversion whichSha) +{ + switch (whichSha) { + case SHA1: return SHA1HashSizeBits; + case SHA224: return SHA224HashSizeBits; + case SHA256: return SHA256HashSizeBits; + case SHA384: return SHA384HashSizeBits; + default: + case SHA512: return SHA512HashSizeBits; + } +} + +/* + * USHAHashName + * + * Description: + * This function will return the name of the given SHA algorithm + * as a string. + * + * Parameters: + * whichSha: + * which SHA algorithm to query + * + * Returns: + * character string with the name in it + * + */ +const char *USHAHashName(enum SHAversion whichSha) +{ + switch (whichSha) { + case SHA1: return "SHA1"; + case SHA224: return "SHA224"; + case SHA256: return "SHA256"; + + + +Eastlake & Hansen Informational [Page 78] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + case SHA384: return "SHA384"; + default: + case SHA512: return "SHA512"; + } +} + +8.3. The HMAC Code + +/**************************** hmac.c ***************************/ +/***************** See RFC 6234 for details. *******************/ +/* Copyright (c) 2011 IETF Trust and the persons identified as */ +/* authors of the code. All rights reserved. */ +/* See sha.h for terms of use and redistribution. */ + +/* + * Description: + * This file implements the HMAC algorithm (Keyed-Hashing for + * Message Authentication, [RFC 2104]), expressed in terms of + * the various SHA algorithms. + */ + +#include "sha.h" + +/* + * hmac + * + * Description: + * This function will compute an HMAC message digest. + * + * Parameters: + * whichSha: [in] + * One of SHA1, SHA224, SHA256, SHA384, SHA512 + * message_array[ ]: [in] + * An array of octets representing the message. + * Note: in RFC 2104, this parameter is known + * as 'text'. + * length: [in] + * The length of the message in message_array. + * key[ ]: [in] + * The secret shared key. + * key_len: [in] + * The length of the secret shared key. + * digest[ ]: [out] + * Where the digest is to be returned. + * NOTE: The length of the digest is determined by + * the value of whichSha. + * + + + + +Eastlake & Hansen Informational [Page 79] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + * Returns: + * sha Error Code. + * + */ + +int hmac(SHAversion whichSha, + const unsigned char *message_array, int length, + const unsigned char *key, int key_len, + uint8_t digest[USHAMaxHashSize]) +{ + HMACContext context; + return hmacReset(&context, whichSha, key, key_len) || + hmacInput(&context, message_array, length) || + hmacResult(&context, digest); +} + +/* + * hmacReset + * + * Description: + * This function will initialize the hmacContext in preparation + * for computing a new HMAC message digest. + * + * Parameters: + * context: [in/out] + * The context to reset. + * whichSha: [in] + * One of SHA1, SHA224, SHA256, SHA384, SHA512 + * key[ ]: [in] + * The secret shared key. + * key_len: [in] + * The length of the secret shared key. + * + * Returns: + * sha Error Code. + * + */ +int hmacReset(HMACContext *context, enum SHAversion whichSha, + const unsigned char *key, int key_len) +{ + int i, blocksize, hashsize, ret; + + /* inner padding - key XORd with ipad */ + unsigned char k_ipad[USHA_Max_Message_Block_Size]; + + /* temporary buffer when keylen > blocksize */ + unsigned char tempkey[USHAMaxHashSize]; + + + + +Eastlake & Hansen Informational [Page 80] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + if (!context) return shaNull; + context->Computed = 0; + context->Corrupted = shaSuccess; + + blocksize = context->blockSize = USHABlockSize(whichSha); + hashsize = context->hashSize = USHAHashSize(whichSha); + context->whichSha = whichSha; + + /* + * If key is longer than the hash blocksize, + * reset it to key = HASH(key). + */ + if (key_len > blocksize) { + USHAContext tcontext; + int err = USHAReset(&tcontext, whichSha) || + USHAInput(&tcontext, key, key_len) || + USHAResult(&tcontext, tempkey); + if (err != shaSuccess) return err; + + key = tempkey; + key_len = hashsize; + } + + /* + * The HMAC transform looks like: + * + * SHA(K XOR opad, SHA(K XOR ipad, text)) + * + * where K is an n byte key, 0-padded to a total of blocksize bytes, + * ipad is the byte 0x36 repeated blocksize times, + * opad is the byte 0x5c repeated blocksize times, + * and text is the data being protected. + */ + + /* store key into the pads, XOR'd with ipad and opad values */ + for (i = 0; i < key_len; i++) { + k_ipad[i] = key[i] ^ 0x36; + context->k_opad[i] = key[i] ^ 0x5c; + } + /* remaining pad bytes are '\0' XOR'd with ipad and opad values */ + for ( ; i < blocksize; i++) { + k_ipad[i] = 0x36; + context->k_opad[i] = 0x5c; + } + + /* perform inner hash */ + /* init context for 1st pass */ + ret = USHAReset(&context->shaContext, whichSha) || + + + +Eastlake & Hansen Informational [Page 81] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + /* and start with inner pad */ + USHAInput(&context->shaContext, k_ipad, blocksize); + return context->Corrupted = ret; +} + +/* + * hmacInput + * + * Description: + * This function accepts an array of octets as the next portion + * of the message. It may be called multiple times. + * + * Parameters: + * context: [in/out] + * The HMAC context to update. + * text[ ]: [in] + * An array of octets representing the next portion of + * the message. + * text_len: [in] + * The length of the message in text. + * + * Returns: + * sha Error Code. + * + */ +int hmacInput(HMACContext *context, const unsigned char *text, + int text_len) +{ + if (!context) return shaNull; + if (context->Corrupted) return context->Corrupted; + if (context->Computed) return context->Corrupted = shaStateError; + /* then text of datagram */ + return context->Corrupted = + USHAInput(&context->shaContext, text, text_len); +} + +/* + * hmacFinalBits + * + * Description: + * This function will add in any final bits of the message. + * + * Parameters: + * context: [in/out] + * The HMAC context to update. + * message_bits: [in] + * The final bits of the message, in the upper portion of the + * byte. (Use 0b###00000 instead of 0b00000### to input the + + + +Eastlake & Hansen Informational [Page 82] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + * three bits ###.) + * length: [in] + * The number of bits in message_bits, between 1 and 7. + * + * Returns: + * sha Error Code. + */ +int hmacFinalBits(HMACContext *context, + uint8_t bits, unsigned int bit_count) +{ + if (!context) return shaNull; + if (context->Corrupted) return context->Corrupted; + if (context->Computed) return context->Corrupted = shaStateError; + /* then final bits of datagram */ + return context->Corrupted = + USHAFinalBits(&context->shaContext, bits, bit_count); +} + +/* + * hmacResult + * + * Description: + * This function will return the N-byte message digest into the + * Message_Digest array provided by the caller. + * + * Parameters: + * context: [in/out] + * The context to use to calculate the HMAC hash. + * digest[ ]: [out] + * Where the digest is returned. + * NOTE 2: The length of the hash is determined by the value of + * whichSha that was passed to hmacReset(). + * + * Returns: + * sha Error Code. + * + */ +int hmacResult(HMACContext *context, uint8_t *digest) +{ + int ret; + if (!context) return shaNull; + if (context->Corrupted) return context->Corrupted; + if (context->Computed) return context->Corrupted = shaStateError; + + /* finish up 1st pass */ + /* (Use digest here as a temporary buffer.) */ + ret = + USHAResult(&context->shaContext, digest) || + + + +Eastlake & Hansen Informational [Page 83] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + /* perform outer SHA */ + /* init context for 2nd pass */ + USHAReset(&context->shaContext, context->whichSha) || + + /* start with outer pad */ + USHAInput(&context->shaContext, context->k_opad, + context->blockSize) || + + /* then results of 1st hash */ + USHAInput(&context->shaContext, digest, context->hashSize) || + /* finish up 2nd pass */ + USHAResult(&context->shaContext, digest); + + context->Computed = 1; + return context->Corrupted = ret; +} + +8.4. The HKDF Code + +/**************************** hkdf.c ***************************/ +/***************** See RFC 6234 for details. *******************/ +/* Copyright (c) 2011 IETF Trust and the persons identified as */ +/* authors of the code. All rights reserved. */ +/* See sha.h for terms of use and redistribution. */ + +/* + * Description: + * This file implements the HKDF algorithm (HMAC-based + * Extract-and-Expand Key Derivation Function, RFC 5869), + * expressed in terms of the various SHA algorithms. + */ + +#include "sha.h" +#include <string.h> +#include <stdlib.h> + +/* + * hkdf + * + * Description: + * This function will generate keying material using HKDF. + * + * Parameters: + * whichSha: [in] + * One of SHA1, SHA224, SHA256, SHA384, SHA512 + * salt[ ]: [in] + * The optional salt value (a non-secret random value); + * if not provided (salt == NULL), it is set internally + + + +Eastlake & Hansen Informational [Page 84] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + * to a string of HashLen(whichSha) zeros. + * salt_len: [in] + * The length of the salt value. (Ignored if salt == NULL.) + * ikm[ ]: [in] + * Input keying material. + * ikm_len: [in] + * The length of the input keying material. + * info[ ]: [in] + * The optional context and application specific information. + * If info == NULL or a zero-length string, it is ignored. + * info_len: [in] + * The length of the optional context and application specific + * information. (Ignored if info == NULL.) + * okm[ ]: [out] + * Where the HKDF is to be stored. + * okm_len: [in] + * The length of the buffer to hold okm. + * okm_len must be <= 255 * USHABlockSize(whichSha) + * + * Notes: + * Calls hkdfExtract() and hkdfExpand(). + * + * Returns: + * sha Error Code. + * + */ +int hkdf(SHAversion whichSha, + const unsigned char *salt, int salt_len, + const unsigned char *ikm, int ikm_len, + const unsigned char *info, int info_len, + uint8_t okm[ ], int okm_len) +{ + uint8_t prk[USHAMaxHashSize]; + return hkdfExtract(whichSha, salt, salt_len, ikm, ikm_len, prk) || + hkdfExpand(whichSha, prk, USHAHashSize(whichSha), info, + info_len, okm, okm_len); +} + +/* + * hkdfExtract + * + * Description: + * This function will perform HKDF extraction. + * + * Parameters: + * whichSha: [in] + * One of SHA1, SHA224, SHA256, SHA384, SHA512 + * salt[ ]: [in] + + + +Eastlake & Hansen Informational [Page 85] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + * The optional salt value (a non-secret random value); + * if not provided (salt == NULL), it is set internally + * to a string of HashLen(whichSha) zeros. + * salt_len: [in] + * The length of the salt value. (Ignored if salt == NULL.) + * ikm[ ]: [in] + * Input keying material. + * ikm_len: [in] + * The length of the input keying material. + * prk[ ]: [out] + * Array where the HKDF extraction is to be stored. + * Must be larger than USHAHashSize(whichSha); + * + * Returns: + * sha Error Code. + * + */ +int hkdfExtract(SHAversion whichSha, + const unsigned char *salt, int salt_len, + const unsigned char *ikm, int ikm_len, + uint8_t prk[USHAMaxHashSize]) +{ + unsigned char nullSalt[USHAMaxHashSize]; + if (salt == 0) { + salt = nullSalt; + salt_len = USHAHashSize(whichSha); + memset(nullSalt, '\0', salt_len); + } else if (salt_len < 0) { + return shaBadParam; + } + return hmac(whichSha, ikm, ikm_len, salt, salt_len, prk); +} + +/* + * hkdfExpand + * + * Description: + * This function will perform HKDF expansion. + * + * Parameters: + * whichSha: [in] + * One of SHA1, SHA224, SHA256, SHA384, SHA512 + * prk[ ]: [in] + * The pseudo-random key to be expanded; either obtained + * directly from a cryptographically strong, uniformly + * distributed pseudo-random number generator, or as the + * output from hkdfExtract(). + * prk_len: [in] + + + +Eastlake & Hansen Informational [Page 86] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + * The length of the pseudo-random key in prk; + * should at least be equal to USHAHashSize(whichSHA). + * info[ ]: [in] + * The optional context and application specific information. + * If info == NULL or a zero-length string, it is ignored. + * info_len: [in] + * The length of the optional context and application specific + * information. (Ignored if info == NULL.) + * okm[ ]: [out] + * Where the HKDF is to be stored. + * okm_len: [in] + * The length of the buffer to hold okm. + * okm_len must be <= 255 * USHABlockSize(whichSha) + * + * Returns: + * sha Error Code. + * + */ +int hkdfExpand(SHAversion whichSha, const uint8_t prk[ ], int prk_len, + const unsigned char *info, int info_len, + uint8_t okm[ ], int okm_len) +{ + int hash_len, N; + unsigned char T[USHAMaxHashSize]; + int Tlen, where, i; + + if (info == 0) { + info = (const unsigned char *)""; + info_len = 0; + } else if (info_len < 0) { + return shaBadParam; + } + if (okm_len <= 0) return shaBadParam; + if (!okm) return shaBadParam; + + hash_len = USHAHashSize(whichSha); + if (prk_len < hash_len) return shaBadParam; + N = okm_len / hash_len; + if ((okm_len % hash_len) != 0) N++; + if (N > 255) return shaBadParam; + + Tlen = 0; + where = 0; + for (i = 1; i <= N; i++) { + HMACContext context; + unsigned char c = i; + int ret = hmacReset(&context, whichSha, prk, prk_len) || + hmacInput(&context, T, Tlen) || + + + +Eastlake & Hansen Informational [Page 87] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + hmacInput(&context, info, info_len) || + hmacInput(&context, &c, 1) || + hmacResult(&context, T); + if (ret != shaSuccess) return ret; + memcpy(okm + where, T, + (i != N) ? hash_len : (okm_len - where)); + where += hash_len; + Tlen = hash_len; + } + return shaSuccess; +} + +/* + * hkdfReset + * + * Description: + * This function will initialize the hkdfContext in preparation + * for key derivation using the modular HKDF interface for + * arbitrary length inputs. + * + * Parameters: + * context: [in/out] + * The context to reset. + * whichSha: [in] + * One of SHA1, SHA224, SHA256, SHA384, SHA512 + * salt[ ]: [in] + * The optional salt value (a non-secret random value); + * if not provided (salt == NULL), it is set internally + * to a string of HashLen(whichSha) zeros. + * salt_len: [in] + * The length of the salt value. (Ignored if salt == NULL.) + * + * Returns: + * sha Error Code. + * + */ +int hkdfReset(HKDFContext *context, enum SHAversion whichSha, + const unsigned char *salt, int salt_len) +{ + unsigned char nullSalt[USHAMaxHashSize]; + if (!context) return shaNull; + + context->whichSha = whichSha; + context->hashSize = USHAHashSize(whichSha); + if (salt == 0) { + salt = nullSalt; + salt_len = context->hashSize; + memset(nullSalt, '\0', salt_len); + + + +Eastlake & Hansen Informational [Page 88] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + } + + return hmacReset(&context->hmacContext, whichSha, salt, salt_len); +} + +/* + * hkdfInput + * + * Description: + * This function accepts an array of octets as the next portion + * of the input keying material. It may be called multiple times. + * + * Parameters: + * context: [in/out] + * The HKDF context to update. + * ikm[ ]: [in] + * An array of octets representing the next portion of + * the input keying material. + * ikm_len: [in] + * The length of ikm. + * + * Returns: + * sha Error Code. + * + */ +int hkdfInput(HKDFContext *context, const unsigned char *ikm, + int ikm_len) +{ + if (!context) return shaNull; + if (context->Corrupted) return context->Corrupted; + if (context->Computed) return context->Corrupted = shaStateError; + return hmacInput(&context->hmacContext, ikm, ikm_len); +} + +/* + * hkdfFinalBits + * + * Description: + * This function will add in any final bits of the + * input keying material. + * + * Parameters: + * context: [in/out] + * The HKDF context to update + * ikm_bits: [in] + * The final bits of the input keying material, in the upper + * portion of the byte. (Use 0b###00000 instead of 0b00000### + * to input the three bits ###.) + + + +Eastlake & Hansen Informational [Page 89] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + * ikm_bit_count: [in] + * The number of bits in message_bits, between 1 and 7. + * + * Returns: + * sha Error Code. + */ +int hkdfFinalBits(HKDFContext *context, uint8_t ikm_bits, + unsigned int ikm_bit_count) +{ + if (!context) return shaNull; + if (context->Corrupted) return context->Corrupted; + if (context->Computed) return context->Corrupted = shaStateError; + return hmacFinalBits(&context->hmacContext, ikm_bits, ikm_bit_count); +} + +/* + * hkdfResult + * + * Description: + * This function will finish the HKDF extraction and perform the + * final HKDF expansion. + * + * Parameters: + * context: [in/out] + * The HKDF context to use to calculate the HKDF hash. + * prk[ ]: [out] + * An optional location to store the HKDF extraction. + * Either NULL, or pointer to a buffer that must be + * larger than USHAHashSize(whichSha); + * info[ ]: [in] + * The optional context and application specific information. + * If info == NULL or a zero-length string, it is ignored. + * info_len: [in] + * The length of the optional context and application specific + * information. (Ignored if info == NULL.) + * okm[ ]: [out] + * Where the HKDF is to be stored. + * okm_len: [in] + * The length of the buffer to hold okm. + * okm_len must be <= 255 * USHABlockSize(whichSha) + * + * Returns: + * sha Error Code. + * + */ +int hkdfResult(HKDFContext *context, + uint8_t prk[USHAMaxHashSize], + const unsigned char *info, int info_len, + + + +Eastlake & Hansen Informational [Page 90] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + uint8_t okm[ ], int okm_len) +{ + uint8_t prkbuf[USHAMaxHashSize]; + int ret; + + if (!context) return shaNull; + if (context->Corrupted) return context->Corrupted; + if (context->Computed) return context->Corrupted = shaStateError; + if (!okm) return context->Corrupted = shaBadParam; + if (!prk) prk = prkbuf; + + ret = hmacResult(&context->hmacContext, prk) || + hkdfExpand(context->whichSha, prk, context->hashSize, info, + info_len, okm, okm_len); + context->Computed = 1; + return context->Corrupted = ret; +} + +8.5. The Test Driver + + The following code is a main program test driver to exercise the code + in sha1.c, sha224-256.c, sha384-512.c, hmac.c, and hkdf.c. The test + driver can also be used as a standalone program for generating the + hashes. Note that the tests assume that character values are as in + [US-ASCII] and a run time check warns if the code appears to have + been compiled with some other character system. + + See also [SHAVS]. + +/************************** shatest.c **************************/ +/***************** See RFC 6234 for details. *******************/ +/* Copyright (c) 2011 IETF Trust and the persons identified as */ +/* authors of the code. All rights reserved. */ +/* See sha.h for terms of use and redistribution. */ + +/* + * Description: + * This file will exercise the SHA code performing + * the three tests documented in FIPS PUB 180-3 + * (http://csrc.nist.gov/publications/fips/ + * fips180-2/fips180-2withchangenotice.pdf) + * one that calls SHAInput with an exact multiple of 512 bits + * the seven tests documented for each algorithm in + * "The Secure Hash Algorithm Validation System (SHAVS)" + * (http://csrc.nist.gov/cryptval/shs/SHAVS.pdf), + * three of which are bit-level tests + * + + + + +Eastlake & Hansen Informational [Page 91] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + * These tests have subsequently been moved to pages linked from + * http://csrc.nist.gov/groups/ST/toolkit/examples.html + * + * This file will exercise the HMAC SHA1 code performing + * the seven tests documented in RFCs [RFC 2202] and [RFC 4231]. + * + * This file will exercise the HKDF code performing + * the seven tests documented in RFC 4869. + * + * To run the tests and just see PASSED/FAILED, use the -p option. + * + * Other options exercise: + * hashing an arbitrary string + * hashing a file's contents + * a few error test checks + * printing the results in raw format + * + * Portability Issues: + * None. + * + */ + +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <ctype.h> +#include <unistd.h> /* defines getopt() and optarg */ +#include "sha.h" + +static int scasecmp(const char *s1, const char *s2); + +/* + * Define patterns for testing + */ +#define TEST1 "abc" +#define TEST2_1 \ + "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq" +#define TEST2_2a \ + "abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmn" +#define TEST2_2b \ + "hijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu" +#define TEST2_2 TEST2_2a TEST2_2b +#define TEST3 "a" /* times 1000000 */ +#define TEST4a "01234567012345670123456701234567" +#define TEST4b "01234567012345670123456701234567" + /* an exact multiple of 512 bits */ +#define TEST4 TEST4a TEST4b /* times 10 */ + + + +Eastlake & Hansen Informational [Page 92] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + +#define TEST7_1 \ + "\x49\xb2\xae\xc2\x59\x4b\xbe\x3a\x3b\x11\x75\x42\xd9\x4a\xc8" +#define TEST8_1 \ + "\x9a\x7d\xfd\xf1\xec\xea\xd0\x6e\xd6\x46\xaa\x55\xfe\x75\x71\x46" +#define TEST9_1 \ + "\x65\xf9\x32\x99\x5b\xa4\xce\x2c\xb1\xb4\xa2\xe7\x1a\xe7\x02\x20" \ + "\xaa\xce\xc8\x96\x2d\xd4\x49\x9c\xbd\x7c\x88\x7a\x94\xea\xaa\x10" \ + "\x1e\xa5\xaa\xbc\x52\x9b\x4e\x7e\x43\x66\x5a\x5a\xf2\xcd\x03\xfe" \ + "\x67\x8e\xa6\xa5\x00\x5b\xba\x3b\x08\x22\x04\xc2\x8b\x91\x09\xf4" \ + "\x69\xda\xc9\x2a\xaa\xb3\xaa\x7c\x11\xa1\xb3\x2a" +#define TEST10_1 \ + "\xf7\x8f\x92\x14\x1b\xcd\x17\x0a\xe8\x9b\x4f\xba\x15\xa1\xd5\x9f" \ + "\x3f\xd8\x4d\x22\x3c\x92\x51\xbd\xac\xbb\xae\x61\xd0\x5e\xd1\x15" \ + "\xa0\x6a\x7c\xe1\x17\xb7\xbe\xea\xd2\x44\x21\xde\xd9\xc3\x25\x92" \ + "\xbd\x57\xed\xea\xe3\x9c\x39\xfa\x1f\xe8\x94\x6a\x84\xd0\xcf\x1f" \ + "\x7b\xee\xad\x17\x13\xe2\xe0\x95\x98\x97\x34\x7f\x67\xc8\x0b\x04" \ + "\x00\xc2\x09\x81\x5d\x6b\x10\xa6\x83\x83\x6f\xd5\x56\x2a\x56\xca" \ + "\xb1\xa2\x8e\x81\xb6\x57\x66\x54\x63\x1c\xf1\x65\x66\xb8\x6e\x3b" \ + "\x33\xa1\x08\xb0\x53\x07\xc0\x0a\xff\x14\xa7\x68\xed\x73\x50\x60" \ + "\x6a\x0f\x85\xe6\xa9\x1d\x39\x6f\x5b\x5c\xbe\x57\x7f\x9b\x38\x80" \ + "\x7c\x7d\x52\x3d\x6d\x79\x2f\x6e\xbc\x24\xa4\xec\xf2\xb3\xa4\x27" \ + "\xcd\xbb\xfb" +#define TEST7_224 \ + "\xf0\x70\x06\xf2\x5a\x0b\xea\x68\xcd\x76\xa2\x95\x87\xc2\x8d" +#define TEST8_224 \ + "\x18\x80\x40\x05\xdd\x4f\xbd\x15\x56\x29\x9d\x6f\x9d\x93\xdf\x62" +#define TEST9_224 \ + "\xa2\xbe\x6e\x46\x32\x81\x09\x02\x94\xd9\xce\x94\x82\x65\x69\x42" \ + "\x3a\x3a\x30\x5e\xd5\xe2\x11\x6c\xd4\xa4\xc9\x87\xfc\x06\x57\x00" \ + "\x64\x91\xb1\x49\xcc\xd4\xb5\x11\x30\xac\x62\xb1\x9d\xc2\x48\xc7" \ + "\x44\x54\x3d\x20\xcd\x39\x52\xdc\xed\x1f\x06\xcc\x3b\x18\xb9\x1f" \ + "\x3f\x55\x63\x3e\xcc\x30\x85\xf4\x90\x70\x60\xd2" +#define TEST10_224 \ + "\x55\xb2\x10\x07\x9c\x61\xb5\x3a\xdd\x52\x06\x22\xd1\xac\x97\xd5" \ + "\xcd\xbe\x8c\xb3\x3a\xa0\xae\x34\x45\x17\xbe\xe4\xd7\xba\x09\xab" \ + "\xc8\x53\x3c\x52\x50\x88\x7a\x43\xbe\xbb\xac\x90\x6c\x2e\x18\x37" \ + "\xf2\x6b\x36\xa5\x9a\xe3\xbe\x78\x14\xd5\x06\x89\x6b\x71\x8b\x2a" \ + "\x38\x3e\xcd\xac\x16\xb9\x61\x25\x55\x3f\x41\x6f\xf3\x2c\x66\x74" \ + "\xc7\x45\x99\xa9\x00\x53\x86\xd9\xce\x11\x12\x24\x5f\x48\xee\x47" \ + "\x0d\x39\x6c\x1e\xd6\x3b\x92\x67\x0c\xa5\x6e\xc8\x4d\xee\xa8\x14" \ + "\xb6\x13\x5e\xca\x54\x39\x2b\xde\xdb\x94\x89\xbc\x9b\x87\x5a\x8b" \ + "\xaf\x0d\xc1\xae\x78\x57\x36\x91\x4a\xb7\xda\xa2\x64\xbc\x07\x9d" \ + "\x26\x9f\x2c\x0d\x7e\xdd\xd8\x10\xa4\x26\x14\x5a\x07\x76\xf6\x7c" \ + "\x87\x82\x73" +#define TEST7_256 \ + "\xbe\x27\x46\xc6\xdb\x52\x76\x5f\xdb\x2f\x88\x70\x0f\x9a\x73" +#define TEST8_256 \ + "\xe3\xd7\x25\x70\xdc\xdd\x78\x7c\xe3\x88\x7a\xb2\xcd\x68\x46\x52" + + + +Eastlake & Hansen Informational [Page 93] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + +#define TEST9_256 \ + "\x3e\x74\x03\x71\xc8\x10\xc2\xb9\x9f\xc0\x4e\x80\x49\x07\xef\x7c" \ + "\xf2\x6b\xe2\x8b\x57\xcb\x58\xa3\xe2\xf3\xc0\x07\x16\x6e\x49\xc1" \ + "\x2e\x9b\xa3\x4c\x01\x04\x06\x91\x29\xea\x76\x15\x64\x25\x45\x70" \ + "\x3a\x2b\xd9\x01\xe1\x6e\xb0\xe0\x5d\xeb\xa0\x14\xeb\xff\x64\x06" \ + "\xa0\x7d\x54\x36\x4e\xff\x74\x2d\xa7\x79\xb0\xb3" +#define TEST10_256 \ + "\x83\x26\x75\x4e\x22\x77\x37\x2f\x4f\xc1\x2b\x20\x52\x7a\xfe\xf0" \ + "\x4d\x8a\x05\x69\x71\xb1\x1a\xd5\x71\x23\xa7\xc1\x37\x76\x00\x00" \ + "\xd7\xbe\xf6\xf3\xc1\xf7\xa9\x08\x3a\xa3\x9d\x81\x0d\xb3\x10\x77" \ + "\x7d\xab\x8b\x1e\x7f\x02\xb8\x4a\x26\xc7\x73\x32\x5f\x8b\x23\x74" \ + "\xde\x7a\x4b\x5a\x58\xcb\x5c\x5c\xf3\x5b\xce\xe6\xfb\x94\x6e\x5b" \ + "\xd6\x94\xfa\x59\x3a\x8b\xeb\x3f\x9d\x65\x92\xec\xed\xaa\x66\xca" \ + "\x82\xa2\x9d\x0c\x51\xbc\xf9\x33\x62\x30\xe5\xd7\x84\xe4\xc0\xa4" \ + "\x3f\x8d\x79\xa3\x0a\x16\x5c\xba\xbe\x45\x2b\x77\x4b\x9c\x71\x09" \ + "\xa9\x7d\x13\x8f\x12\x92\x28\x96\x6f\x6c\x0a\xdc\x10\x6a\xad\x5a" \ + "\x9f\xdd\x30\x82\x57\x69\xb2\xc6\x71\xaf\x67\x59\xdf\x28\xeb\x39" \ + "\x3d\x54\xd6" +#define TEST7_384 \ + "\x8b\xc5\x00\xc7\x7c\xee\xd9\x87\x9d\xa9\x89\x10\x7c\xe0\xaa" +#define TEST8_384 \ + "\xa4\x1c\x49\x77\x79\xc0\x37\x5f\xf1\x0a\x7f\x4e\x08\x59\x17\x39" +#define TEST9_384 \ + "\x68\xf5\x01\x79\x2d\xea\x97\x96\x76\x70\x22\xd9\x3d\xa7\x16\x79" \ + "\x30\x99\x20\xfa\x10\x12\xae\xa3\x57\xb2\xb1\x33\x1d\x40\xa1\xd0" \ + "\x3c\x41\xc2\x40\xb3\xc9\xa7\x5b\x48\x92\xf4\xc0\x72\x4b\x68\xc8" \ + "\x75\x32\x1a\xb8\xcf\xe5\x02\x3b\xd3\x75\xbc\x0f\x94\xbd\x89\xfe" \ + "\x04\xf2\x97\x10\x5d\x7b\x82\xff\xc0\x02\x1a\xeb\x1c\xcb\x67\x4f" \ + "\x52\x44\xea\x34\x97\xde\x26\xa4\x19\x1c\x5f\x62\xe5\xe9\xa2\xd8" \ + "\x08\x2f\x05\x51\xf4\xa5\x30\x68\x26\xe9\x1c\xc0\x06\xce\x1b\xf6" \ + "\x0f\xf7\x19\xd4\x2f\xa5\x21\xc8\x71\xcd\x23\x94\xd9\x6e\xf4\x46" \ + "\x8f\x21\x96\x6b\x41\xf2\xba\x80\xc2\x6e\x83\xa9" +#define TEST10_384 \ + "\x39\x96\x69\xe2\x8f\x6b\x9c\x6d\xbc\xbb\x69\x12\xec\x10\xff\xcf" \ + "\x74\x79\x03\x49\xb7\xdc\x8f\xbe\x4a\x8e\x7b\x3b\x56\x21\xdb\x0f" \ + "\x3e\x7d\xc8\x7f\x82\x32\x64\xbb\xe4\x0d\x18\x11\xc9\xea\x20\x61" \ + "\xe1\xc8\x4a\xd1\x0a\x23\xfa\xc1\x72\x7e\x72\x02\xfc\x3f\x50\x42" \ + "\xe6\xbf\x58\xcb\xa8\xa2\x74\x6e\x1f\x64\xf9\xb9\xea\x35\x2c\x71" \ + "\x15\x07\x05\x3c\xf4\xe5\x33\x9d\x52\x86\x5f\x25\xcc\x22\xb5\xe8" \ + "\x77\x84\xa1\x2f\xc9\x61\xd6\x6c\xb6\xe8\x95\x73\x19\x9a\x2c\xe6" \ + "\x56\x5c\xbd\xf1\x3d\xca\x40\x38\x32\xcf\xcb\x0e\x8b\x72\x11\xe8" \ + "\x3a\xf3\x2a\x11\xac\x17\x92\x9f\xf1\xc0\x73\xa5\x1c\xc0\x27\xaa" \ + "\xed\xef\xf8\x5a\xad\x7c\x2b\x7c\x5a\x80\x3e\x24\x04\xd9\x6d\x2a" \ + "\x77\x35\x7b\xda\x1a\x6d\xae\xed\x17\x15\x1c\xb9\xbc\x51\x25\xa4" \ + "\x22\xe9\x41\xde\x0c\xa0\xfc\x50\x11\xc2\x3e\xcf\xfe\xfd\xd0\x96" \ + "\x76\x71\x1c\xf3\xdb\x0a\x34\x40\x72\x0e\x16\x15\xc1\xf2\x2f\xbc" \ + "\x3c\x72\x1d\xe5\x21\xe1\xb9\x9b\xa1\xbd\x55\x77\x40\x86\x42\x14" \ + "\x7e\xd0\x96" + + + +Eastlake & Hansen Informational [Page 94] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + +#define TEST7_512 \ + "\x08\xec\xb5\x2e\xba\xe1\xf7\x42\x2d\xb6\x2b\xcd\x54\x26\x70" +#define TEST8_512 \ + "\x8d\x4e\x3c\x0e\x38\x89\x19\x14\x91\x81\x6e\x9d\x98\xbf\xf0\xa0" +#define TEST9_512 \ + "\x3a\xdd\xec\x85\x59\x32\x16\xd1\x61\x9a\xa0\x2d\x97\x56\x97\x0b" \ + "\xfc\x70\xac\xe2\x74\x4f\x7c\x6b\x27\x88\x15\x10\x28\xf7\xb6\xa2" \ + "\x55\x0f\xd7\x4a\x7e\x6e\x69\xc2\xc9\xb4\x5f\xc4\x54\x96\x6d\xc3" \ + "\x1d\x2e\x10\xda\x1f\x95\xce\x02\xbe\xb4\xbf\x87\x65\x57\x4c\xbd" \ + "\x6e\x83\x37\xef\x42\x0a\xdc\x98\xc1\x5c\xb6\xd5\xe4\xa0\x24\x1b" \ + "\xa0\x04\x6d\x25\x0e\x51\x02\x31\xca\xc2\x04\x6c\x99\x16\x06\xab" \ + "\x4e\xe4\x14\x5b\xee\x2f\xf4\xbb\x12\x3a\xab\x49\x8d\x9d\x44\x79" \ + "\x4f\x99\xcc\xad\x89\xa9\xa1\x62\x12\x59\xed\xa7\x0a\x5b\x6d\xd4" \ + "\xbd\xd8\x77\x78\xc9\x04\x3b\x93\x84\xf5\x49\x06" +#define TEST10_512 \ + "\xa5\x5f\x20\xc4\x11\xaa\xd1\x32\x80\x7a\x50\x2d\x65\x82\x4e\x31" \ + "\xa2\x30\x54\x32\xaa\x3d\x06\xd3\xe2\x82\xa8\xd8\x4e\x0d\xe1\xde" \ + "\x69\x74\xbf\x49\x54\x69\xfc\x7f\x33\x8f\x80\x54\xd5\x8c\x26\xc4" \ + "\x93\x60\xc3\xe8\x7a\xf5\x65\x23\xac\xf6\xd8\x9d\x03\xe5\x6f\xf2" \ + "\xf8\x68\x00\x2b\xc3\xe4\x31\xed\xc4\x4d\xf2\xf0\x22\x3d\x4b\xb3" \ + "\xb2\x43\x58\x6e\x1a\x7d\x92\x49\x36\x69\x4f\xcb\xba\xf8\x8d\x95" \ + "\x19\xe4\xeb\x50\xa6\x44\xf8\xe4\xf9\x5e\xb0\xea\x95\xbc\x44\x65" \ + "\xc8\x82\x1a\xac\xd2\xfe\x15\xab\x49\x81\x16\x4b\xbb\x6d\xc3\x2f" \ + "\x96\x90\x87\xa1\x45\xb0\xd9\xcc\x9c\x67\xc2\x2b\x76\x32\x99\x41" \ + "\x9c\xc4\x12\x8b\xe9\xa0\x77\xb3\xac\xe6\x34\x06\x4e\x6d\x99\x28" \ + "\x35\x13\xdc\x06\xe7\x51\x5d\x0d\x73\x13\x2e\x9a\x0d\xc6\xd3\xb1" \ + "\xf8\xb2\x46\xf1\xa9\x8a\x3f\xc7\x29\x41\xb1\xe3\xbb\x20\x98\xe8" \ + "\xbf\x16\xf2\x68\xd6\x4f\x0b\x0f\x47\x07\xfe\x1e\xa1\xa1\x79\x1b" \ + "\xa2\xf3\xc0\xc7\x58\xe5\xf5\x51\x86\x3a\x96\xc9\x49\xad\x47\xd7" \ + "\xfb\x40\xd2" +#define SHA1_SEED "\xd0\x56\x9c\xb3\x66\x5a\x8a\x43\xeb\x6e\xa2\x3d" \ + "\x75\xa3\xc4\xd2\x05\x4a\x0d\x7d" +#define SHA224_SEED "\xd0\x56\x9c\xb3\x66\x5a\x8a\x43\xeb\x6e\xa2" \ + "\x3d\x75\xa3\xc4\xd2\x05\x4a\x0d\x7d\x66\xa9\xca\x99\xc9\xce\xb0" \ + "\x27" +#define SHA256_SEED "\xf4\x1e\xce\x26\x13\xe4\x57\x39\x15\x69\x6b" \ + "\x5a\xdc\xd5\x1c\xa3\x28\xbe\x3b\xf5\x66\xa9\xca\x99\xc9\xce\xb0" \ + "\x27\x9c\x1c\xb0\xa7" +#define SHA384_SEED "\x82\x40\xbc\x51\xe4\xec\x7e\xf7\x6d\x18\xe3" \ + "\x52\x04\xa1\x9f\x51\xa5\x21\x3a\x73\xa8\x1d\x6f\x94\x46\x80\xd3" \ + "\x07\x59\x48\xb7\xe4\x63\x80\x4e\xa3\xd2\x6e\x13\xea\x82\x0d\x65" \ + "\xa4\x84\xbe\x74\x53" +#define SHA512_SEED "\x47\x3f\xf1\xb9\xb3\xff\xdf\xa1\x26\x69\x9a" \ + "\xc7\xef\x9e\x8e\x78\x77\x73\x09\x58\x24\xc6\x42\x55\x7c\x13\x99" \ + "\xd9\x8e\x42\x20\x44\x8d\xc3\x5b\x99\xbf\xdd\x44\x77\x95\x43\x92" \ + "\x4c\x1c\xe9\x3b\xc5\x94\x15\x38\x89\x5d\xb9\x88\x26\x1b\x00\x77" \ + "\x4b\x12\x27\x20\x39" + + + + +Eastlake & Hansen Informational [Page 95] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + +#define TESTCOUNT 10 +#define HASHCOUNT 5 +#define RANDOMCOUNT 4 +#define HMACTESTCOUNT 7 +#define HKDFTESTCOUNT 7 + +#define PRINTNONE 0 +#define PRINTTEXT 1 +#define PRINTRAW 2 +#define PRINTHEX 3 +#define PRINTBASE64 4 + +#define PRINTPASSFAIL 1 +#define PRINTFAIL 2 + +#define length(x) (sizeof(x)-1) + +/* Test arrays for hashes. */ +struct hash { + const char *name; + SHAversion whichSha; + int hashsize; + struct { + const char *testarray; + int length; + long repeatcount; + int extrabits; + int numberExtrabits; + const char *resultarray; + } tests[TESTCOUNT]; + const char *randomtest; + const char *randomresults[RANDOMCOUNT]; +} hashes[HASHCOUNT] = { + { "SHA1", SHA1, SHA1HashSize, + { + /* 1 */ { TEST1, length(TEST1), 1, 0, 0, + "A9993E364706816ABA3E25717850C26C9CD0D89D" }, + /* 2 */ { TEST2_1, length(TEST2_1), 1, 0, 0, + "84983E441C3BD26EBAAE4AA1F95129E5E54670F1" }, + /* 3 */ { TEST3, length(TEST3), 1000000, 0, 0, + "34AA973CD4C4DAA4F61EEB2BDBAD27316534016F" }, + /* 4 */ { TEST4, length(TEST4), 10, 0, 0, + "DEA356A2CDDD90C7A7ECEDC5EBB563934F460452" }, + /* 5 */ { "", 0, 0, 0x98, 5, + "29826B003B906E660EFF4027CE98AF3531AC75BA" }, + /* 6 */ { "\x5e", 1, 1, 0, 0, + "5E6F80A34A9798CAFC6A5DB96CC57BA4C4DB59C2" }, + /* 7 */ { TEST7_1, length(TEST7_1), 1, 0x80, 3, + + + +Eastlake & Hansen Informational [Page 96] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + "6239781E03729919C01955B3FFA8ACB60B988340" }, + /* 8 */ { TEST8_1, length(TEST8_1), 1, 0, 0, + "82ABFF6605DBE1C17DEF12A394FA22A82B544A35" }, + /* 9 */ { TEST9_1, length(TEST9_1), 1, 0xE0, 3, + "8C5B2A5DDAE5A97FC7F9D85661C672ADBF7933D4" }, + /* 10 */ { TEST10_1, length(TEST10_1), 1, 0, 0, + "CB0082C8F197D260991BA6A460E76E202BAD27B3" } + }, SHA1_SEED, { "E216836819477C7F78E0D843FE4FF1B6D6C14CD4", + "A2DBC7A5B1C6C0A8BCB7AAA41252A6A7D0690DBC", + "DB1F9050BB863DFEF4CE37186044E2EEB17EE013", + "127FDEDF43D372A51D5747C48FBFFE38EF6CDF7B" + } }, + { "SHA224", SHA224, SHA224HashSize, + { + /* 1 */ { TEST1, length(TEST1), 1, 0, 0, + "23097D223405D8228642A477BDA255B32AADBCE4BDA0B3F7E36C9DA7" }, + /* 2 */ { TEST2_1, length(TEST2_1), 1, 0, 0, + "75388B16512776CC5DBA5DA1FD890150B0C6455CB4F58B1952522525" }, + /* 3 */ { TEST3, length(TEST3), 1000000, 0, 0, + "20794655980C91D8BBB4C1EA97618A4BF03F42581948B2EE4EE7AD67" }, + /* 4 */ { TEST4, length(TEST4), 10, 0, 0, + "567F69F168CD7844E65259CE658FE7AADFA25216E68ECA0EB7AB8262" }, + /* 5 */ { "", 0, 0, 0x68, 5, + "E3B048552C3C387BCAB37F6EB06BB79B96A4AEE5FF27F51531A9551C" }, + /* 6 */ { "\x07", 1, 1, 0, 0, + "00ECD5F138422B8AD74C9799FD826C531BAD2FCABC7450BEE2AA8C2A" }, + /* 7 */ { TEST7_224, length(TEST7_224), 1, 0xA0, 3, + "1B01DB6CB4A9E43DED1516BEB3DB0B87B6D1EA43187462C608137150" }, + /* 8 */ { TEST8_224, length(TEST8_224), 1, 0, 0, + "DF90D78AA78821C99B40BA4C966921ACCD8FFB1E98AC388E56191DB1" }, + /* 9 */ { TEST9_224, length(TEST9_224), 1, 0xE0, 3, + "54BEA6EAB8195A2EB0A7906A4B4A876666300EEFBD1F3B8474F9CD57" }, + /* 10 */ { TEST10_224, length(TEST10_224), 1, 0, 0, + "0B31894EC8937AD9B91BDFBCBA294D9ADEFAA18E09305E9F20D5C3A4" } + }, SHA224_SEED, { "100966A5B4FDE0B42E2A6C5953D4D7F41BA7CF79FD" + "2DF431416734BE", "1DCA396B0C417715DEFAAE9641E10A2E99D55A" + "BCB8A00061EB3BE8BD", "1864E627BDB2319973CD5ED7D68DA71D8B" + "F0F983D8D9AB32C34ADB34", "A2406481FC1BCAF24DD08E6752E844" + "709563FB916227FED598EB621F" + } }, + { "SHA256", SHA256, SHA256HashSize, + { + /* 1 */ { TEST1, length(TEST1), 1, 0, 0, "BA7816BF8F01CFEA4141" + "40DE5DAE2223B00361A396177A9CB410FF61F20015AD" }, + /* 2 */ { TEST2_1, length(TEST2_1), 1, 0, 0, "248D6A61D20638B8" + "E5C026930C3E6039A33CE45964FF2167F6ECEDD419DB06C1" }, + /* 3 */ { TEST3, length(TEST3), 1000000, 0, 0, "CDC76E5C9914FB92" + "81A1C7E284D73E67F1809A48A497200E046D39CCC7112CD0" }, + + + +Eastlake & Hansen Informational [Page 97] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + /* 4 */ { TEST4, length(TEST4), 10, 0, 0, "594847328451BDFA" + "85056225462CC1D867D877FB388DF0CE35F25AB5562BFBB5" }, + /* 5 */ { "", 0, 0, 0x68, 5, "D6D3E02A31A84A8CAA9718ED6C2057BE" + "09DB45E7823EB5079CE7A573A3760F95" }, + /* 6 */ { "\x19", 1, 1, 0, 0, "68AA2E2EE5DFF96E3355E6C7EE373E3D" + "6A4E17F75F9518D843709C0C9BC3E3D4" }, + /* 7 */ { TEST7_256, length(TEST7_256), 1, 0x60, 3, "77EC1DC8" + "9C821FF2A1279089FA091B35B8CD960BCAF7DE01C6A7680756BEB972" }, + /* 8 */ { TEST8_256, length(TEST8_256), 1, 0, 0, "175EE69B02BA" + "9B58E2B0A5FD13819CEA573F3940A94F825128CF4209BEABB4E8" }, + /* 9 */ { TEST9_256, length(TEST9_256), 1, 0xA0, 3, "3E9AD646" + "8BBBAD2AC3C2CDC292E018BA5FD70B960CF1679777FCE708FDB066E9" }, + /* 10 */ { TEST10_256, length(TEST10_256), 1, 0, 0, "97DBCA7D" + "F46D62C8A422C941DD7E835B8AD3361763F7E9B2D95F4F0DA6E1CCBC" }, + }, SHA256_SEED, { "83D28614D49C3ADC1D6FC05DB5F48037C056F8D2A4CE44" + "EC6457DEA5DD797CD1", "99DBE3127EF2E93DD9322D6A07909EB33B6399" + "5E529B3F954B8581621BB74D39", "8D4BE295BB64661CA3C7EFD129A2F7" + "25B33072DBDDE32385B9A87B9AF88EA76F", "40AF5D3F9716B040DF9408" + "E31536B70FF906EC51B00447CA97D7DD97C12411F4" + } }, + { "SHA384", SHA384, SHA384HashSize, + { + /* 1 */ { TEST1, length(TEST1), 1, 0, 0, + "CB00753F45A35E8BB5A03D699AC65007272C32AB0EDED163" + "1A8B605A43FF5BED8086072BA1E7CC2358BAECA134C825A7" }, + /* 2 */ { TEST2_2, length(TEST2_2), 1, 0, 0, + "09330C33F71147E83D192FC782CD1B4753111B173B3B05D2" + "2FA08086E3B0F712FCC7C71A557E2DB966C3E9FA91746039" }, + /* 3 */ { TEST3, length(TEST3), 1000000, 0, 0, + "9D0E1809716474CB086E834E310A4A1CED149E9C00F24852" + "7972CEC5704C2A5B07B8B3DC38ECC4EBAE97DDD87F3D8985" }, + /* 4 */ { TEST4, length(TEST4), 10, 0, 0, + "2FC64A4F500DDB6828F6A3430B8DD72A368EB7F3A8322A70" + "BC84275B9C0B3AB00D27A5CC3C2D224AA6B61A0D79FB4596" }, + /* 5 */ { "", 0, 0, 0x10, 5, + "8D17BE79E32B6718E07D8A603EB84BA0478F7FCFD1BB9399" + "5F7D1149E09143AC1FFCFC56820E469F3878D957A15A3FE4" }, + /* 6 */ { "\xb9", 1, 1, 0, 0, + "BC8089A19007C0B14195F4ECC74094FEC64F01F90929282C" + "2FB392881578208AD466828B1C6C283D2722CF0AD1AB6938" }, + /* 7 */ { TEST7_384, length(TEST7_384), 1, 0xA0, 3, + "D8C43B38E12E7C42A7C9B810299FD6A770BEF30920F17532" + "A898DE62C7A07E4293449C0B5FA70109F0783211CFC4BCE3" }, + /* 8 */ { TEST8_384, length(TEST8_384), 1, 0, 0, + "C9A68443A005812256B8EC76B00516F0DBB74FAB26D66591" + "3F194B6FFB0E91EA9967566B58109CBC675CC208E4C823F7" }, + /* 9 */ { TEST9_384, length(TEST9_384), 1, 0xE0, 3, + "5860E8DE91C21578BB4174D227898A98E0B45C4C760F0095" + + + +Eastlake & Hansen Informational [Page 98] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + "49495614DAEDC0775D92D11D9F8CE9B064EEAC8DAFC3A297" }, + /* 10 */ { TEST10_384, length(TEST10_384), 1, 0, 0, + "4F440DB1E6EDD2899FA335F09515AA025EE177A79F4B4AAF" + "38E42B5C4DE660F5DE8FB2A5B2FBD2A3CBFFD20CFF1288C0" } + }, SHA384_SEED, { "CE44D7D63AE0C91482998CF662A51EC80BF6FC68661A3C" + "57F87566112BD635A743EA904DEB7D7A42AC808CABE697F38F", "F9C6D2" + "61881FEE41ACD39E67AA8D0BAD507C7363EB67E2B81F45759F9C0FD7B503" + "DF1A0B9E80BDE7BC333D75B804197D", "D96512D8C9F4A7A4967A366C01" + "C6FD97384225B58343A88264847C18E4EF8AB7AEE4765FFBC3E30BD485D3" + "638A01418F", "0CA76BD0813AF1509E170907A96005938BC985628290B2" + "5FEF73CF6FAD68DDBA0AC8920C94E0541607B0915A7B4457F7" + } }, + { "SHA512", SHA512, SHA512HashSize, + { + /* 1 */ { TEST1, length(TEST1), 1, 0, 0, + "DDAF35A193617ABACC417349AE20413112E6FA4E89A97EA2" + "0A9EEEE64B55D39A2192992A274FC1A836BA3C23A3FEEBBD" + "454D4423643CE80E2A9AC94FA54CA49F" }, + /* 2 */ { TEST2_2, length(TEST2_2), 1, 0, 0, + "8E959B75DAE313DA8CF4F72814FC143F8F7779C6EB9F7FA1" + "7299AEADB6889018501D289E4900F7E4331B99DEC4B5433A" + "C7D329EEB6DD26545E96E55B874BE909" }, + /* 3 */ { TEST3, length(TEST3), 1000000, 0, 0, + "E718483D0CE769644E2E42C7BC15B4638E1F98B13B204428" + "5632A803AFA973EBDE0FF244877EA60A4CB0432CE577C31B" + "EB009C5C2C49AA2E4EADB217AD8CC09B" }, + /* 4 */ { TEST4, length(TEST4), 10, 0, 0, + "89D05BA632C699C31231DED4FFC127D5A894DAD412C0E024" + "DB872D1ABD2BA8141A0F85072A9BE1E2AA04CF33C765CB51" + "0813A39CD5A84C4ACAA64D3F3FB7BAE9" }, + /* 5 */ { "", 0, 0, 0xB0, 5, + "D4EE29A9E90985446B913CF1D1376C836F4BE2C1CF3CADA0" + "720A6BF4857D886A7ECB3C4E4C0FA8C7F95214E41DC1B0D2" + "1B22A84CC03BF8CE4845F34DD5BDBAD4" }, + /* 6 */ { "\xD0", 1, 1, 0, 0, + "9992202938E882E73E20F6B69E68A0A7149090423D93C81B" + "AB3F21678D4ACEEEE50E4E8CAFADA4C85A54EA8306826C4A" + "D6E74CECE9631BFA8A549B4AB3FBBA15" }, + /* 7 */ { TEST7_512, length(TEST7_512), 1, 0x80, 3, + "ED8DC78E8B01B69750053DBB7A0A9EDA0FB9E9D292B1ED71" + "5E80A7FE290A4E16664FD913E85854400C5AF05E6DAD316B" + "7359B43E64F8BEC3C1F237119986BBB6" }, + /* 8 */ { TEST8_512, length(TEST8_512), 1, 0, 0, + "CB0B67A4B8712CD73C9AABC0B199E9269B20844AFB75ACBD" + "D1C153C9828924C3DDEDAAFE669C5FDD0BC66F630F677398" + "8213EB1B16F517AD0DE4B2F0C95C90F8" }, + /* 9 */ { TEST9_512, length(TEST9_512), 1, 0x80, 3, + "32BA76FC30EAA0208AEB50FFB5AF1864FDBF17902A4DC0A6" + + + +Eastlake & Hansen Informational [Page 99] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + "82C61FCEA6D92B783267B21080301837F59DE79C6B337DB2" + "526F8A0A510E5E53CAFED4355FE7C2F1" }, + /* 10 */ { TEST10_512, length(TEST10_512), 1, 0, 0, + "C665BEFB36DA189D78822D10528CBF3B12B3EEF726039909" + "C1A16A270D48719377966B957A878E720584779A62825C18" + "DA26415E49A7176A894E7510FD1451F5" } + }, SHA512_SEED, { "2FBB1E7E00F746BA514FBC8C421F36792EC0E11FF5EFC3" + "78E1AB0C079AA5F0F66A1E3EDBAEB4F9984BE14437123038A452004A5576" + "8C1FD8EED49E4A21BEDCD0", "25CBE5A4F2C7B1D7EF07011705D50C62C5" + "000594243EAFD1241FC9F3D22B58184AE2FEE38E171CF8129E29459C9BC2" + "EF461AF5708887315F15419D8D17FE7949", "5B8B1F2687555CE2D7182B" + "92E5C3F6C36547DA1C13DBB9EA4F73EA4CBBAF89411527906D35B1B06C1B" + "6A8007D05EC66DF0A406066829EAB618BDE3976515AAFC", "46E36B007D" + "19876CDB0B29AD074FE3C08CDD174D42169D6ABE5A1414B6E79707DF5877" + "6A98091CF431854147BB6D3C66D43BFBC108FD715BDE6AA127C2B0E79F" + } + } +}; + +/* Test arrays for HMAC. */ +struct hmachash { + const char *keyarray[5]; + int keylength[5]; + const char *dataarray[5]; + int datalength[5]; + const char *resultarray[5]; + int resultlength[5]; +} hmachashes[HMACTESTCOUNT] = { + { /* 1 */ { + "\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b" + "\x0b\x0b\x0b\x0b\x0b" + }, { 20 }, { + "\x48\x69\x20\x54\x68\x65\x72\x65" /* "Hi There" */ + }, { 8 }, { + /* HMAC-SHA-1 */ + "B617318655057264E28BC0B6FB378C8EF146BE00", + /* HMAC-SHA-224 */ + "896FB1128ABBDF196832107CD49DF33F47B4B1169912BA4F53684B22", + /* HMAC-SHA-256 */ + "B0344C61D8DB38535CA8AFCEAF0BF12B881DC200C9833DA726E9376C2E32" + "CFF7", + /* HMAC-SHA-384 */ + "AFD03944D84895626B0825F4AB46907F15F9DADBE4101EC682AA034C7CEB" + "C59CFAEA9EA9076EDE7F4AF152E8B2FA9CB6", + /* HMAC-SHA-512 */ + "87AA7CDEA5EF619D4FF0B4241A1D6CB02379F4E2CE4EC2787AD0B30545E1" + "7CDEDAA833B7D6B8A702038B274EAEA3F4E4BE9D914EEB61F1702E696C20" + "3A126854" + + + +Eastlake & Hansen Informational [Page 100] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + }, { SHA1HashSize, SHA224HashSize, SHA256HashSize, + SHA384HashSize, SHA512HashSize } + }, + { /* 2 */ { + "\x4a\x65\x66\x65" /* "Jefe" */ + }, { 4 }, { + "\x77\x68\x61\x74\x20\x64\x6f\x20\x79\x61\x20\x77\x61\x6e\x74" + "\x20\x66\x6f\x72\x20\x6e\x6f\x74\x68\x69\x6e\x67\x3f" + /* "what do ya want for nothing?" */ + }, { 28 }, { + /* HMAC-SHA-1 */ + "EFFCDF6AE5EB2FA2D27416D5F184DF9C259A7C79", + /* HMAC-SHA-224 */ + "A30E01098BC6DBBF45690F3A7E9E6D0F8BBEA2A39E6148008FD05E44", + /* HMAC-SHA-256 */ + "5BDCC146BF60754E6A042426089575C75A003F089D2739839DEC58B964EC" + "3843", + /* HMAC-SHA-384 */ + "AF45D2E376484031617F78D2B58A6B1B9C7EF464F5A01B47E42EC3736322" + "445E8E2240CA5E69E2C78B3239ECFAB21649", + /* HMAC-SHA-512 */ + "164B7A7BFCF819E2E395FBE73B56E0A387BD64222E831FD610270CD7EA25" + "05549758BF75C05A994A6D034F65F8F0E6FDCAEAB1A34D4A6B4B636E070A" + "38BCE737" + }, { SHA1HashSize, SHA224HashSize, SHA256HashSize, + SHA384HashSize, SHA512HashSize } + }, + { /* 3 */ + { + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa" + }, { 20 }, { + "\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd" + "\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd" + "\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd" + "\xdd\xdd\xdd\xdd\xdd" + }, { 50 }, { + /* HMAC-SHA-1 */ + "125D7342B9AC11CD91A39AF48AA17B4F63F175D3", + /* HMAC-SHA-224 */ + "7FB3CB3588C6C1F6FFA9694D7D6AD2649365B0C1F65D69D1EC8333EA", + /* HMAC-SHA-256 */ + "773EA91E36800E46854DB8EBD09181A72959098B3EF8C122D9635514CED5" + "65FE", + /* HMAC-SHA-384 */ + "88062608D3E6AD8A0AA2ACE014C8A86F0AA635D947AC9FEBE83EF4E55966" + "144B2A5AB39DC13814B94E3AB6E101A34F27", + /* HMAC-SHA-512 */ + + + +Eastlake & Hansen Informational [Page 101] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + "FA73B0089D56A284EFB0F0756C890BE9B1B5DBDD8EE81A3655F83E33B227" + "9D39BF3E848279A722C806B485A47E67C807B946A337BEE8942674278859" + "E13292FB" + }, { SHA1HashSize, SHA224HashSize, SHA256HashSize, + SHA384HashSize, SHA512HashSize } + }, + { /* 4 */ { + "\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" + "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19" + }, { 25 }, { + "\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd" + "\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd" + "\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd" + "\xcd\xcd\xcd\xcd\xcd" + }, { 50 }, { + /* HMAC-SHA-1 */ + "4C9007F4026250C6BC8414F9BF50C86C2D7235DA", + /* HMAC-SHA-224 */ + "6C11506874013CAC6A2ABC1BB382627CEC6A90D86EFC012DE7AFEC5A", + /* HMAC-SHA-256 */ + "82558A389A443C0EA4CC819899F2083A85F0FAA3E578F8077A2E3FF46729" + "665B", + /* HMAC-SHA-384 */ + "3E8A69B7783C25851933AB6290AF6CA77A9981480850009CC5577C6E1F57" + "3B4E6801DD23C4A7D679CCF8A386C674CFFB", + /* HMAC-SHA-512 */ + "B0BA465637458C6990E5A8C5F61D4AF7E576D97FF94B872DE76F8050361E" + "E3DBA91CA5C11AA25EB4D679275CC5788063A5F19741120C4F2DE2ADEBEB" + "10A298DD" + }, { SHA1HashSize, SHA224HashSize, SHA256HashSize, + SHA384HashSize, SHA512HashSize } + }, + { /* 5 */ { + "\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c" + "\x0c\x0c\x0c\x0c\x0c" + }, { 20 }, { + "Test With Truncation" + }, { 20 }, { + /* HMAC-SHA-1 */ + "4C1A03424B55E07FE7F27BE1", + /* HMAC-SHA-224 */ + "0E2AEA68A90C8D37C988BCDB9FCA6FA8", + /* HMAC-SHA-256 */ + "A3B6167473100EE06E0C796C2955552B", + /* HMAC-SHA-384 */ + "3ABF34C3503B2A23A46EFC619BAEF897", + /* HMAC-SHA-512 */ + "415FAD6271580A531D4179BC891D87A6" + + + +Eastlake & Hansen Informational [Page 102] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + }, { 12, 16, 16, 16, 16 } + }, + { /* 6 */ { + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + }, { 80, 131 }, { + "Test Using Larger Than Block-Size Key - Hash Key First" + }, { 54 }, { + /* HMAC-SHA-1 */ + "AA4AE5E15272D00E95705637CE8A3B55ED402112", + /* HMAC-SHA-224 */ + "95E9A0DB962095ADAEBE9B2D6F0DBCE2D499F112F2D2B7273FA6870E", + /* HMAC-SHA-256 */ + "60E431591EE0B67F0D8A26AACBF5B77F8E0BC6213728C5140546040F0EE3" + "7F54", + /* HMAC-SHA-384 */ + "4ECE084485813E9088D2C63A041BC5B44F9EF1012A2B588F3CD11F05033A" + "C4C60C2EF6AB4030FE8296248DF163F44952", + /* HMAC-SHA-512 */ + "80B24263C7C1A3EBB71493C1DD7BE8B49B46D1F41B4AEEC1121B013783F8" + "F3526B56D037E05F2598BD0FD2215D6A1E5295E64F73F63F0AEC8B915A98" + "5D786598" + }, { SHA1HashSize, SHA224HashSize, SHA256HashSize, + SHA384HashSize, SHA512HashSize } + }, + { /* 7 */ { + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" + }, { 80, 131 }, { + "Test Using Larger Than Block-Size Key and " + "Larger Than One Block-Size Data", + "\x54\x68\x69\x73\x20\x69\x73\x20\x61\x20\x74\x65\x73\x74\x20" + "\x75\x73\x69\x6e\x67\x20\x61\x20\x6c\x61\x72\x67\x65\x72\x20" + "\x74\x68\x61\x6e\x20\x62\x6c\x6f\x63\x6b\x2d\x73\x69\x7a\x65" + + + +Eastlake & Hansen Informational [Page 103] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + "\x20\x6b\x65\x79\x20\x61\x6e\x64\x20\x61\x20\x6c\x61\x72\x67" + "\x65\x72\x20\x74\x68\x61\x6e\x20\x62\x6c\x6f\x63\x6b\x2d\x73" + "\x69\x7a\x65\x20\x64\x61\x74\x61\x2e\x20\x54\x68\x65\x20\x6b" + "\x65\x79\x20\x6e\x65\x65\x64\x73\x20\x74\x6f\x20\x62\x65\x20" + "\x68\x61\x73\x68\x65\x64\x20\x62\x65\x66\x6f\x72\x65\x20\x62" + "\x65\x69\x6e\x67\x20\x75\x73\x65\x64\x20\x62\x79\x20\x74\x68" + "\x65\x20\x48\x4d\x41\x43\x20\x61\x6c\x67\x6f\x72\x69\x74\x68" + "\x6d\x2e" + /* "This is a test using a larger than block-size key and a " + "larger than block-size data. The key needs to be hashed " + "before being used by the HMAC algorithm." */ + }, { 73, 152 }, { + /* HMAC-SHA-1 */ + "E8E99D0F45237D786D6BBAA7965C7808BBFF1A91", + /* HMAC-SHA-224 */ + "3A854166AC5D9F023F54D517D0B39DBD946770DB9C2B95C9F6F565D1", + /* HMAC-SHA-256 */ + "9B09FFA71B942FCB27635FBCD5B0E944BFDC63644F0713938A7F51535C3A" + "35E2", + /* HMAC-SHA-384 */ + "6617178E941F020D351E2F254E8FD32C602420FEB0B8FB9ADCCEBB82461E" + "99C5A678CC31E799176D3860E6110C46523E", + /* HMAC-SHA-512 */ + "E37B6A775DC87DBAA4DFA9F96E5E3FFDDEBD71F8867289865DF5A32D20CD" + "C944B6022CAC3C4982B10D5EEB55C3E4DE15134676FB6DE0446065C97440" + "FA8C6A58" + }, { SHA1HashSize, SHA224HashSize, SHA256HashSize, + SHA384HashSize, SHA512HashSize } + } +}; + +/* Test arrays for HKDF. */ +struct hkdfhash { + SHAversion whichSha; + int ikmlength; + const char *ikmarray; + int saltlength; + const char *saltarray; + int infolength; + const char *infoarray; + int prklength; + const char *prkarray; + int okmlength; + const char *okmarray; +} hkdfhashes[HKDFTESTCOUNT] = { + { /* RFC 5869 A.1. Test Case 1 */ + SHA256, + 22, "\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b" + + + +Eastlake & Hansen Informational [Page 104] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + "\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b", + 13, "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c", + 10, "\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9", + 32, "077709362C2E32DF0DDC3F0DC47BBA6390B6C73BB50F9C3122EC844A" + "D7C2B3E5", + 42, "3CB25F25FAACD57A90434F64D0362F2A2D2D0A90CF1A5A4C5DB02D56" + "ECC4C5BF34007208D5B887185865" + }, + { /* RFC 5869 A.2. Test Case 2 */ + SHA256, + 80, "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d" + "\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b" + "\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29" + "\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37" + "\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45" + "\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f", + 80, "\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d" + "\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b" + "\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89" + "\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97" + "\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5" + "\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf", + 80, "\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd" + "\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb" + "\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9" + "\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7" + "\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5" + "\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff", + 32, "06A6B88C5853361A06104C9CEB35B45C" + "EF760014904671014A193F40C15FC244", + 82, "B11E398DC80327A1C8E7F78C596A4934" + "4F012EDA2D4EFAD8A050CC4C19AFA97C" + "59045A99CAC7827271CB41C65E590E09" + "DA3275600C2F09B8367793A9ACA3DB71" + "CC30C58179EC3E87C14C01D5C1F3434F" + "1D87" + }, + { /* RFC 5869 A.3. Test Case 3 */ + SHA256, + 22, "\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b" + "\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b", + 0, "", + 0, "", + 32, "19EF24A32C717B167F33A91D6F648BDF" + "96596776AFDB6377AC434C1C293CCB04", + 42, "8DA4E775A563C18F715F802A063C5A31" + "B8A11F5C5EE1879EC3454E5F3C738D2D" + "9D201395FAA4B61A96C8" + + + +Eastlake & Hansen Informational [Page 105] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + }, + { /* RFC 5869 A.4. Test Case 4 */ + SHA1, + 11, "\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b", + 13, "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c", + 10, "\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9", + 20, "9B6C18C432A7BF8F0E71C8EB88F4B30BAA2BA243", + 42, "085A01EA1B10F36933068B56EFA5AD81" + "A4F14B822F5B091568A9CDD4F155FDA2" + "C22E422478D305F3F896" + }, + { /* RFC 5869 A.5. Test Case 5 */ + SHA1, + 80, "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d" + "\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b" + "\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29" + "\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37" + "\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45" + "\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f", + 80, "\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6A\x6B\x6C\x6D" + "\x6E\x6F\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7A\x7B" + "\x7C\x7D\x7E\x7F\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89" + "\x8A\x8B\x8C\x8D\x8E\x8F\x90\x91\x92\x93\x94\x95\x96\x97" + "\x98\x99\x9A\x9B\x9C\x9D\x9E\x9F\xA0\xA1\xA2\xA3\xA4\xA5" + "\xA6\xA7\xA8\xA9\xAA\xAB\xAC\xAD\xAE\xAF", + 80, "\xB0\xB1\xB2\xB3\xB4\xB5\xB6\xB7\xB8\xB9\xBA\xBB\xBC\xBD" + "\xBE\xBF\xC0\xC1\xC2\xC3\xC4\xC5\xC6\xC7\xC8\xC9\xCA\xCB" + "\xCC\xCD\xCE\xCF\xD0\xD1\xD2\xD3\xD4\xD5\xD6\xD7\xD8\xD9" + "\xDA\xDB\xDC\xDD\xDE\xDF\xE0\xE1\xE2\xE3\xE4\xE5\xE6\xE7" + "\xE8\xE9\xEA\xEB\xEC\xED\xEE\xEF\xF0\xF1\xF2\xF3\xF4\xF5" + "\xF6\xF7\xF8\xF9\xFA\xFB\xFC\xFD\xFE\xFF", + 20, "8ADAE09A2A307059478D309B26C4115A224CFAF6", + 82, "0BD770A74D1160F7C9F12CD5912A06EB" + "FF6ADCAE899D92191FE4305673BA2FFE" + "8FA3F1A4E5AD79F3F334B3B202B2173C" + "486EA37CE3D397ED034C7F9DFEB15C5E" + "927336D0441F4C4300E2CFF0D0900B52" + "D3B4" + }, + { /* RFC 5869 A.6. Test Case 6 */ + SHA1, + 22, "\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b" + "\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b", + 0, "", + 0, "", + 20, "DA8C8A73C7FA77288EC6F5E7C297786AA0D32D01", + 42, "0AC1AF7002B3D761D1E55298DA9D0506" + "B9AE52057220A306E07B6B87E8DF21D0" + + + +Eastlake & Hansen Informational [Page 106] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + "EA00033DE03984D34918" + }, + { /* RFC 5869 A.7. Test Case 7. */ + SHA1, + 22, "\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c" + "\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c", + 0, 0, + 0, "", + 20, "2ADCCADA18779E7C2077AD2EB19D3F3E731385DD", + 42, "2C91117204D745F3500D636A62F64F0A" + "B3BAE548AA53D423B0D1F27EBBA6F5E5" + "673A081D70CCE7ACFC48" + } +}; + +/* + * Check the hash value against the expected string, expressed in hex + */ +static const char hexdigits[ ] = "0123456789ABCDEF"; +int checkmatch(const unsigned char *hashvalue, + const char *hexstr, int hashsize) +{ + int i; + for (i = 0; i < hashsize; ++i) { + if (*hexstr++ != hexdigits[(hashvalue[i] >> 4) & 0xF]) + return 0; + if (*hexstr++ != hexdigits[hashvalue[i] & 0xF]) return 0; + } + return 1; +} + +/* + * Print the string, converting non-printable characters to "." + */ +void printstr(const char *str, int len) +{ + for ( ; len-- > 0; str++) + putchar(isprint((unsigned char)*str) ? *str : '.'); +} + +/* + * Print the string, converting all characters to hex "## ". + */ +void printxstr(const char *str, int len) +{ + char *sep = ""; + for ( ; len-- > 0; str++) { + printf("%s%c%c", sep, hexdigits[(*str >> 4) & 0xF], + + + +Eastlake & Hansen Informational [Page 107] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + hexdigits[*str & 0xF]); + sep = " "; + } +} + +/* + * Print a usage message. + */ +void usage(const char *argv0) +{ + fprintf(stderr, + "Usage:\n" + "Common options: [-h hash] [-w|-x|-6] [-H]\n" + "Hash a string:\n" + "\t%s [-S expectedresult] -s hashstr [-k key] " + "[-i info -L okm-len]\n" + "Hash a file:\n" + "\t%s [-S expectedresult] -f file [-k key] " + "[-i info -L okm-len]\n" + "Hash a file, ignoring whitespace:\n" + "\t%s [-S expectedresult] -F file [-k key] " + "[-i info -L okm-len]\n" + "Additional bits to add in: [-B bitcount -b bits]\n" + "(If -k,-i&-L are used, run HKDF-SHA###.\n" + " If -k is used, but not -i&-L, run HMAC-SHA###.\n" + " Otherwise, run SHA###.)\n" + "Standard tests:\n" + "\t%s [-m | -d] [-l loopcount] [-t test#] [-e]\n" + "\t\t[-r randomseed] [-R randomloop-count] " + "[-p] [-P|-X]\n" + "-h\thash to test: " + "0|SHA1, 1|SHA224, 2|SHA256, 3|SHA384, 4|SHA512\n" + "-m\tperform hmac standard tests\n" + "-k\tkey for hmac test\n" + "-d\tperform hkdf standard tests\n" + "-t\ttest case to run, 1-10\n" + "-l\thow many times to run the test\n" + "-e\ttest error returns\n" + "-p\tdo not print results\n" + "-P\tdo not print PASSED/FAILED\n" + "-X\tprint FAILED, but not PASSED\n" + "-r\tseed for random test\n" + "-R\thow many times to run random test\n" + "-s\tstring to hash\n" + "-S\texpected result of hashed string, in hex\n" + "-w\toutput hash in raw format\n" + "-x\toutput hash in hex format\n" + "-6\toutput hash in base64 format\n" + + + +Eastlake & Hansen Informational [Page 108] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + "-B\t# extra bits to add in after string or file input\n" + "-b\textra bits to add (high order bits of #, 0# or 0x#)\n" + "-H\tinput hashstr or randomseed is in hex\n" + , argv0, argv0, argv0, argv0); + exit(1); +} + +/* + * Print the results and PASS/FAIL. + */ +void printResult(uint8_t *Message_Digest, int hashsize, + const char *hashname, const char *testtype, const char *testname, + const char *resultarray, int printResults, int printPassFail) +{ + int i, k; + if (printResults == PRINTTEXT) { + printf("\nhashsize=%d\n", hashsize); + putchar('\t'); + for (i = 0; i < hashsize; ++i) { + putchar(hexdigits[(Message_Digest[i] >> 4) & 0xF]); + putchar(hexdigits[Message_Digest[i] & 0xF]); + putchar(' '); + } + putchar('\n'); + } else if (printResults == PRINTRAW) { + fwrite(Message_Digest, 1, hashsize, stdout); + } else if (printResults == PRINTHEX) { + for (i = 0; i < hashsize; ++i) { + putchar(hexdigits[(Message_Digest[i] >> 4) & 0xF]); + putchar(hexdigits[Message_Digest[i] & 0xF]); + } + putchar('\n'); + } else if (printResults == PRINTBASE64) { + unsigned char b; + char *sm = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + for (i = 0; i < hashsize; i += 3) { + putchar(sm[Message_Digest[i] >> 2]); + b = (Message_Digest[i] & 0x03) << 4; + if (i+1 < hashsize) b |= Message_Digest[i+1] >> 4; + putchar(sm[b]); + if (i+1 < hashsize) { + b = (Message_Digest[i+1] & 0x0f) << 2; + if (i+2 < hashsize) b |= Message_Digest[i+2] >> 6; + putchar(sm[b]); + } else putchar('='); + if (i+2 < hashsize) putchar(sm[Message_Digest[i+2] & 0x3f]); + else putchar('='); + + + +Eastlake & Hansen Informational [Page 109] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + } + putchar('\n'); + } + + if (printResults && resultarray) { + printf(" Should match:\n\t"); + for (i = 0, k = 0; i < hashsize; i++, k += 2) { + putchar(resultarray[k]); + putchar(resultarray[k+1]); + putchar(' '); + } + putchar('\n'); + } + + if (printPassFail && resultarray) { + int ret = checkmatch(Message_Digest, resultarray, hashsize); + if ((printPassFail == PRINTPASSFAIL) || !ret) + printf("%s %s %s: %s\n", hashname, testtype, testname, + ret ? "PASSED" : "FAILED"); + } +} + +/* + * Exercise a hash series of functions. The input is the testarray, + * repeated repeatcount times, followed by the extrabits. If the + * result is known, it is in resultarray in uppercase hex. + */ +int hash(int testno, int loopno, int hashno, + const char *testarray, int length, long repeatcount, + int numberExtrabits, int extrabits, const unsigned char *keyarray, + int keylen, const unsigned char *info, int infolen, int okmlen, + const char *resultarray, int hashsize, int printResults, + int printPassFail) +{ + USHAContext sha; + HMACContext hmac; + HKDFContext hkdf; + int err, i; + uint8_t Message_Digest_Buf[USHAMaxHashSize]; + uint8_t *Message_Digest = Message_Digest_Buf; + char buf[20]; + + if (printResults == PRINTTEXT) { + printf("\nTest %d: Iteration %d, Repeat %ld\n\t'", testno+1, + loopno, repeatcount); + printstr(testarray, length); + printf("'\n\t'"); + printxstr(testarray, length); + + + +Eastlake & Hansen Informational [Page 110] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + printf("'\n"); + printf(" Length=%d bytes (%d bits), ", length, length * 8); + printf("ExtraBits %d: %2.2x\n", numberExtrabits, extrabits); + } + + if (info) Message_Digest = malloc(okmlen); + memset(&sha, '\343', sizeof(sha)); /* force bad data into struct */ + memset(&hmac, '\343', sizeof(hmac)); + memset(&hkdf, '\343', sizeof(hkdf)); + + err = info ? hkdfReset(&hkdf, hashes[hashno].whichSha, + keyarray, keylen) : + keyarray ? hmacReset(&hmac, hashes[hashno].whichSha, + keyarray, keylen) : + USHAReset(&sha, hashes[hashno].whichSha); + if (err != shaSuccess) { + fprintf(stderr, "hash(): %sReset Error %d.\n", + info ? "hkdf" : keyarray ? "hmac" : "sha", err); + return err; + } + + for (i = 0; i < repeatcount; ++i) { + err = info ? hkdfInput(&hkdf, (const uint8_t *)testarray, length) : + keyarray ? hmacInput(&hmac, (const uint8_t *) testarray, + length) : + USHAInput(&sha, (const uint8_t *) testarray, + length); + if (err != shaSuccess) { + fprintf(stderr, "hash(): %sInput Error %d.\n", + info ? "hkdf" : keyarray ? "hmac" : "sha", err); + return err; + } + } + + if (numberExtrabits > 0) { + err = info ? hkdfFinalBits(&hkdf, extrabits, numberExtrabits) : + keyarray ? hmacFinalBits(&hmac, (uint8_t) extrabits, + numberExtrabits) : + USHAFinalBits(&sha, (uint8_t) extrabits, + numberExtrabits); + if (err != shaSuccess) { + fprintf(stderr, "hash(): %sFinalBits Error %d.\n", + info ? "hkdf" : keyarray ? "hmac" : "sha", err); + return err; + } + } + + err = info ? hkdfResult(&hkdf, 0, info, infolen, + + + +Eastlake & Hansen Informational [Page 111] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + Message_Digest, okmlen) : + keyarray ? hmacResult(&hmac, Message_Digest) : + USHAResult(&sha, Message_Digest); + if (err != shaSuccess) { + fprintf(stderr, "hash(): %s Result Error %d, could not compute " + "message digest.\n", + info ? "hkdf" : keyarray ? "hmac" : "sha", err); + return err; + } + + sprintf(buf, "%d", testno+1); + printResult(Message_Digest, info ? okmlen : hashsize, + hashes[hashno].name, info ? "hkdf standard test" : + keyarray ? "hmac standard test" : "sha standard test", buf, + resultarray, printResults, printPassFail); + + return err; +} + +/* + * Exercise an HKDF series. The input is the testarray, + * repeated repeatcount times, followed by the extrabits. If the + * result is known, it is in resultarray in uppercase hex. + */ +int hashHkdf(int testno, int loopno, int hashno, + int printResults, int printPassFail) +{ + int err; + unsigned char prk[USHAMaxHashSize+1]; + uint8_t okm[255 * USHAMaxHashSize+1]; + char buf[20]; + + if (printResults == PRINTTEXT) { + printf("\nTest %d: Iteration %d\n\tSALT\t'", testno+1, loopno); + printxstr(hkdfhashes[testno].saltarray, + hkdfhashes[testno].saltlength); + printf("'\n\tIKM\t'"); + printxstr(hkdfhashes[testno].ikmarray, + hkdfhashes[testno].ikmlength); + printf("'\n\tINFO\t'"); + printxstr(hkdfhashes[testno].infoarray, + hkdfhashes[testno].infolength); + printf("'\n"); + printf(" L=%d bytes\n", hkdfhashes[testno].okmlength); + } + + /* Run hkdf() against the test vectors */ + err = hkdf(hkdfhashes[testno].whichSha, + + + +Eastlake & Hansen Informational [Page 112] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + (const uint8_t *) hkdfhashes[testno].saltarray, + hkdfhashes[testno].saltlength, + (const uint8_t *) hkdfhashes[testno].ikmarray, + hkdfhashes[testno].ikmlength, + (const uint8_t *) hkdfhashes[testno].infoarray, + hkdfhashes[testno].infolength, okm, + hkdfhashes[testno].okmlength); + if (err != shaSuccess) { + fprintf(stderr, "hashHkdf(): hkdf Error %d.\n", err); + return err; + } + sprintf(buf, "hkdf %d", testno+1); + printResult(okm, hkdfhashes[testno].okmlength, + USHAHashName(hkdfhashes[testno].whichSha), "hkdf standard test", + buf, hkdfhashes[testno].okmarray, printResults, printPassFail); + + /* Now run hkdfExtract() by itself against the test vectors */ + /* to verify the intermediate results. */ + err = hkdfExtract(hkdfhashes[testno].whichSha, + (const uint8_t *) hkdfhashes[testno].saltarray, + hkdfhashes[testno].saltlength, + (const uint8_t *) hkdfhashes[testno].ikmarray, + hkdfhashes[testno].ikmlength, prk); + if (err != shaSuccess) { + fprintf(stderr, "hashHkdf(): hkdfExtract Error %d.\n", err); + return err; + } + sprintf(buf, "hkdfExtract %d", testno+1); + printResult(prk, USHAHashSize(hkdfhashes[testno].whichSha), + USHAHashName(hkdfhashes[testno].whichSha), "hkdf standard test", + buf, hkdfhashes[testno].prkarray, printResults, printPassFail); + + /* Now run hkdfExpand() by itself against the test vectors */ + /* using the intermediate results from hkdfExtract. */ + err = hkdfExpand(hkdfhashes[testno].whichSha, prk, + USHAHashSize(hkdfhashes[testno].whichSha), + (const uint8_t *)hkdfhashes[testno].infoarray, + hkdfhashes[testno].infolength, okm, hkdfhashes[testno].okmlength); + if (err != shaSuccess) { + fprintf(stderr, "hashHkdf(): hkdfExpand Error %d.\n", err); + return err; + } + sprintf(buf, "hkdfExpand %d", testno+1); + printResult(okm, hkdfhashes[testno].okmlength, + USHAHashName(hkdfhashes[testno].whichSha), "hkdf standard test", + buf, hkdfhashes[testno].okmarray, printResults, printPassFail); + + return err; + + + +Eastlake & Hansen Informational [Page 113] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + +} + +/* + * Exercise a hash series of functions. The input is a filename. + * If the result is known, it is in resultarray in uppercase hex. + */ +int hashfile(int hashno, const char *hashfilename, int bits, + int bitcount, int skipSpaces, const unsigned char *keyarray, + int keylen, const unsigned char *info, int infolen, int okmlen, + const char *resultarray, int hashsize, + int printResults, int printPassFail) +{ + USHAContext sha; + HMACContext hmac; + HKDFContext hkdf; + int err, nread, c; + unsigned char buf[4096]; + uint8_t Message_Digest_Buf[USHAMaxHashSize]; + uint8_t *Message_Digest = Message_Digest_Buf; + unsigned char cc; + FILE *hashfp = (strcmp(hashfilename, "-") == 0) ? stdin : + fopen(hashfilename, "r"); + + if (!hashfp) { + fprintf(stderr, "cannot open file '%s'\n", hashfilename); + return shaStateError; + } + + if (info) Message_Digest = malloc(okmlen); + memset(&sha, '\343', sizeof(sha)); /* force bad data into struct */ + memset(&hmac, '\343', sizeof(hmac)); + memset(&hkdf, '\343', sizeof(hkdf)); + err = info ? hkdfReset(&hkdf, hashes[hashno].whichSha, + keyarray, keylen) : + keyarray ? hmacReset(&hmac, hashes[hashno].whichSha, + keyarray, keylen) : + USHAReset(&sha, hashes[hashno].whichSha); + if (err != shaSuccess) { + fprintf(stderr, "hashfile(): %sReset Error %d.\n", + info ? "hkdf" : keyarray ? "hmac" : "sha", err); + return err; + } + + if (skipSpaces) + while ((c = getc(hashfp)) != EOF) { + if (!isspace(c)) { + cc = (unsigned char)c; + err = info ? hkdfInput(&hkdf, &cc, 1) : + + + +Eastlake & Hansen Informational [Page 114] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + keyarray ? hmacInput(&hmac, &cc, 1) : + USHAInput(&sha, &cc, 1); + if (err != shaSuccess) { + fprintf(stderr, "hashfile(): %sInput Error %d.\n", + info ? "hkdf" : keyarray ? "hmac" : "sha", err); + if (hashfp != stdin) fclose(hashfp); + return err; + } + } + } + else + while ((nread = fread(buf, 1, sizeof(buf), hashfp)) > 0) { + err = info ? hkdfInput(&hkdf, buf, nread) : + keyarray ? hmacInput(&hmac, buf, nread) : + USHAInput(&sha, buf, nread); + if (err != shaSuccess) { + fprintf(stderr, "hashfile(): %s Error %d.\n", + info ? "hkdf" : keyarray ? "hmacInput" : + "shaInput", err); + if (hashfp != stdin) fclose(hashfp); + return err; + } + } + + if (bitcount > 0) + err = info ? hkdfFinalBits(&hkdf, bits, bitcount) : + keyarray ? hmacFinalBits(&hmac, bits, bitcount) : + USHAFinalBits(&sha, bits, bitcount); + if (err != shaSuccess) { + fprintf(stderr, "hashfile(): %s Error %d.\n", + info ? "hkdf" : keyarray ? "hmacFinalBits" : + "shaFinalBits", err); + if (hashfp != stdin) fclose(hashfp); + return err; + } + + err = info ? hkdfResult(&hkdf, 0, info, infolen, + Message_Digest, okmlen) : + keyarray ? hmacResult(&hmac, Message_Digest) : + USHAResult(&sha, Message_Digest); + if (err != shaSuccess) { + fprintf(stderr, "hashfile(): %s Error %d.\n", + info ? "hkdf" : keyarray ? "hmacResult" : + "shaResult", err); + if (hashfp != stdin) fclose(hashfp); + return err; + } + + + + +Eastlake & Hansen Informational [Page 115] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + printResult(Message_Digest, info ? okmlen : hashsize, + hashes[hashno].name, "file", hashfilename, resultarray, + printResults, printPassFail); + + if (hashfp != stdin) fclose(hashfp); + if (info) free(Message_Digest); + return err; +} + +/* + * Exercise a hash series of functions through multiple permutations. + * The input is an initial seed. That seed is replicated 3 times. + * For 1000 rounds, the previous three results are used as the input. + * This result is then checked, and used to seed the next cycle. + * If the result is known, it is in resultarrays in uppercase hex. + */ +void randomtest(int hashno, const char *seed, int hashsize, + const char **resultarrays, int randomcount, + int printResults, int printPassFail) +{ + int i, j; char buf[20]; + unsigned char SEED[USHAMaxHashSize], MD[1003][USHAMaxHashSize]; + + /* INPUT: Seed - A random seed n bits long */ + memcpy(SEED, seed, hashsize); + if (printResults == PRINTTEXT) { + printf("%s random test seed= '", hashes[hashno].name); + printxstr(seed, hashsize); + printf("'\n"); + } + + for (j = 0; j < randomcount; j++) { + /* MD0 = MD1 = MD2 = Seed; */ + memcpy(MD[0], SEED, hashsize); + memcpy(MD[1], SEED, hashsize); + memcpy(MD[2], SEED, hashsize); + for (i=3; i<1003; i++) { + /* Mi = MDi-3 || MDi-2 || MDi-1; */ + USHAContext Mi; + memset(&Mi, '\343', sizeof(Mi)); /* force bad data into struct */ + USHAReset(&Mi, hashes[hashno].whichSha); + USHAInput(&Mi, MD[i-3], hashsize); + USHAInput(&Mi, MD[i-2], hashsize); + USHAInput(&Mi, MD[i-1], hashsize); + /* MDi = SHA(Mi); */ + USHAResult(&Mi, MD[i]); + } + + + + +Eastlake & Hansen Informational [Page 116] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + /* MDj = Seed = MDi; */ + memcpy(SEED, MD[i-1], hashsize); + + /* OUTPUT: MDj */ + sprintf(buf, "%d", j); + printResult(SEED, hashsize, hashes[hashno].name, "random test", + buf, resultarrays ? resultarrays[j] : 0, printResults, + (j < RANDOMCOUNT) ? printPassFail : 0); + } +} + +/* + * Look up a hash name. + */ +int findhash(const char *argv0, const char *opt) +{ + int i; + const char *names[HASHCOUNT][2] = { + { "0", "sha1" }, { "1", "sha224" }, { "2", "sha256" }, + { "3", "sha384" }, { "4", "sha512" } + }; + for (i = 0; i < HASHCOUNT; i++) + if ((strcmp(opt, names[i][0]) == 0) || + (scasecmp(opt, names[i][1]) == 0)) + return i; + + fprintf(stderr, "%s: Unknown hash name: '%s'\n", argv0, opt); + usage(argv0); + return 0; +} + +/* + * Run some tests that should invoke errors. + */ +void testErrors(int hashnolow, int hashnohigh, int printResults, + int printPassFail) +{ + USHAContext usha; + uint8_t Message_Digest[USHAMaxHashSize]; + int hashno, err; + + for (hashno = hashnolow; hashno <= hashnohigh; hashno++) { + memset(&usha, '\343', sizeof(usha)); /* force bad data */ + USHAReset(&usha, hashno); + USHAResult(&usha, Message_Digest); + err = USHAInput(&usha, (const unsigned char *)"foo", 3); + if (printResults == PRINTTEXT) + printf ("\nError %d. Should be %d.\n", err, shaStateError); + + + +Eastlake & Hansen Informational [Page 117] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + if ((printPassFail == PRINTPASSFAIL) || + ((printPassFail == PRINTFAIL) && (err != shaStateError))) + printf("%s se: %s\n", hashes[hashno].name, + (err == shaStateError) ? "PASSED" : "FAILED"); + + err = USHAFinalBits(&usha, 0x80, 3); + if (printResults == PRINTTEXT) + printf ("\nError %d. Should be %d.\n", err, shaStateError); + if ((printPassFail == PRINTPASSFAIL) || + ((printPassFail == PRINTFAIL) && (err != shaStateError))) + printf("%s se: %s\n", hashes[hashno].name, + (err == shaStateError) ? "PASSED" : "FAILED"); + + err = USHAReset(0, hashes[hashno].whichSha); + if (printResults == PRINTTEXT) + printf("\nError %d. Should be %d.\n", err, shaNull); + if ((printPassFail == PRINTPASSFAIL) || + ((printPassFail == PRINTFAIL) && (err != shaNull))) + printf("%s usha null: %s\n", hashes[hashno].name, + (err == shaNull) ? "PASSED" : "FAILED"); + + switch (hashno) { + case SHA1: err = SHA1Reset(0); break; + case SHA224: err = SHA224Reset(0); break; + case SHA256: err = SHA256Reset(0); break; + case SHA384: err = SHA384Reset(0); break; + case SHA512: err = SHA512Reset(0); break; + } + if (printResults == PRINTTEXT) + printf("\nError %d. Should be %d.\n", err, shaNull); + if ((printPassFail == PRINTPASSFAIL) || + ((printPassFail == PRINTFAIL) && (err != shaNull))) + printf("%s sha null: %s\n", hashes[hashno].name, + (err == shaNull) ? "PASSED" : "FAILED"); + } +} + +/* replace a hex string in place with its value */ +int unhexStr(char *hexstr) +{ + char *o = hexstr; + int len = 0, nibble1 = 0, nibble2 = 0; + if (!hexstr) return 0; + for ( ; *hexstr; hexstr++) { + if (isalpha((int)(unsigned char)(*hexstr))) { + nibble1 = tolower((int)(unsigned char)(*hexstr)) - 'a' + 10; + } else if (isdigit((int)(unsigned char)(*hexstr))) { + nibble1 = *hexstr - '0'; + + + +Eastlake & Hansen Informational [Page 118] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + } else { + printf("\nError: bad hex character '%c'\n", *hexstr); + } + if (!*++hexstr) break; + if (isalpha((int)(unsigned char)(*hexstr))) { + nibble2 = tolower((int)(unsigned char)(*hexstr)) - 'a' + 10; + } else if (isdigit((int)(unsigned char)(*hexstr))) { + nibble2 = *hexstr - '0'; + } else { + printf("\nError: bad hex character '%c'\n", *hexstr); + } + *o++ = (char)((nibble1 << 4) | nibble2); + len++; + } + return len; +} + +int main(int argc, char **argv) +{ + int i, err; + int loopno, loopnohigh = 1; + int hashno, hashnolow = 0, hashnohigh = HASHCOUNT - 1; + int testno, testnolow = 0, testnohigh; + int ntestnohigh = 0; + int printResults = PRINTTEXT; + int printPassFail = 1; + int checkErrors = 0; + char *hashstr = 0; + int hashlen = 0; + const char *resultstr = 0; + char *randomseedstr = 0; + int runHmacTests = 0; + int runHkdfTests = 0; + char *hmacKey = 0; + int hmaclen = 0; + char *info = 0; + int infolen = 0, okmlen = 0; + int randomcount = RANDOMCOUNT; + const char *hashfilename = 0; + const char *hashFilename = 0; + int extrabits = 0, numberExtrabits = 0; + int strIsHex = 0; + + if ('A' != 0x41) { + fprintf(stderr, "%s: these tests require ASCII\n", argv[0]); + } + + while ((i = getopt(argc, argv, + + + +Eastlake & Hansen Informational [Page 119] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + "6b:B:def:F:h:i:Hk:l:L:mpPr:R:s:S:t:wxX")) != -1) + switch (i) { + case 'b': extrabits = strtol(optarg, 0, 0); break; + case 'B': numberExtrabits = atoi(optarg); break; + case 'd': runHkdfTests = 1; break; + case 'e': checkErrors = 1; break; + case 'f': hashfilename = optarg; break; + case 'F': hashFilename = optarg; break; + case 'h': hashnolow = hashnohigh = findhash(argv[0], optarg); + break; + case 'H': strIsHex = 1; break; + case 'i': info = optarg; infolen = strlen(optarg); break; + case 'k': hmacKey = optarg; hmaclen = strlen(optarg); break; + case 'l': loopnohigh = atoi(optarg); break; + case 'L': okmlen = strtol(optarg, 0, 0); break; + case 'm': runHmacTests = 1; break; + case 'P': printPassFail = 0; break; + case 'p': printResults = PRINTNONE; break; + case 'R': randomcount = atoi(optarg); break; + case 'r': randomseedstr = optarg; break; + case 's': hashstr = optarg; hashlen = strlen(hashstr); break; + case 'S': resultstr = optarg; break; + case 't': testnolow = ntestnohigh = atoi(optarg) - 1; break; + case 'w': printResults = PRINTRAW; break; + case 'x': printResults = PRINTHEX; break; + case 'X': printPassFail = 2; break; + case '6': printResults = PRINTBASE64; break; + default: usage(argv[0]); + } + + if (strIsHex) { + hashlen = unhexStr(hashstr); + unhexStr(randomseedstr); + hmaclen = unhexStr(hmacKey); + infolen = unhexStr(info); + } + testnohigh = (ntestnohigh != 0) ? ntestnohigh: + runHmacTests ? (HMACTESTCOUNT-1) : + runHkdfTests ? (HKDFTESTCOUNT-1) : + (TESTCOUNT-1); + if ((testnolow < 0) || + (testnohigh >= (runHmacTests ? HMACTESTCOUNT : TESTCOUNT)) || + (hashnolow < 0) || (hashnohigh >= HASHCOUNT) || + (hashstr && (testnolow == testnohigh)) || + (randomcount < 0) || + (resultstr && (!hashstr && !hashfilename && !hashFilename)) || + ((runHmacTests || hmacKey) && randomseedstr) || + (hashfilename && hashFilename) || + + + +Eastlake & Hansen Informational [Page 120] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + (info && ((infolen <= 0) || (okmlen <= 0))) || + (info && !hmacKey)) + usage(argv[0]); + + /* + * Perform SHA/HMAC tests + */ + for (hashno = hashnolow; hashno <= hashnohigh; ++hashno) { + if (printResults == PRINTTEXT) + printf("Hash %s\n", hashes[hashno].name); + err = shaSuccess; + + for (loopno = 1; (loopno <= loopnohigh) && (err == shaSuccess); + ++loopno) { + if (hashstr) + err = hash(0, loopno, hashno, hashstr, hashlen, 1, + numberExtrabits, extrabits, (const unsigned char *)hmacKey, + hmaclen, (const uint8_t *) info, infolen, okmlen, resultstr, + hashes[hashno].hashsize, printResults, printPassFail); + + else if (randomseedstr) + randomtest(hashno, randomseedstr, hashes[hashno].hashsize, 0, + randomcount, printResults, printPassFail); + + else if (hashfilename) + err = hashfile(hashno, hashfilename, extrabits, + numberExtrabits, 0, + (const unsigned char *)hmacKey, hmaclen, + (const uint8_t *) info, infolen, okmlen, + resultstr, hashes[hashno].hashsize, + printResults, printPassFail); + + else if (hashFilename) + err = hashfile(hashno, hashFilename, extrabits, + numberExtrabits, 1, + (const unsigned char *)hmacKey, hmaclen, + (const uint8_t *) info, infolen, okmlen, + resultstr, hashes[hashno].hashsize, + printResults, printPassFail); + + else /* standard tests */ { + for (testno = testnolow; + (testno <= testnohigh) && (err == shaSuccess); ++testno) { + if (runHmacTests) { + err = hash(testno, loopno, hashno, + hmachashes[testno].dataarray[hashno] ? + hmachashes[testno].dataarray[hashno] : + hmachashes[testno].dataarray[1] ? + + + +Eastlake & Hansen Informational [Page 121] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + hmachashes[testno].dataarray[1] : + hmachashes[testno].dataarray[0], + hmachashes[testno].datalength[hashno] ? + hmachashes[testno].datalength[hashno] : + hmachashes[testno].datalength[1] ? + hmachashes[testno].datalength[1] : + hmachashes[testno].datalength[0], + 1, 0, 0, + (const unsigned char *)( + hmachashes[testno].keyarray[hashno] ? + hmachashes[testno].keyarray[hashno] : + hmachashes[testno].keyarray[1] ? + hmachashes[testno].keyarray[1] : + hmachashes[testno].keyarray[0]), + hmachashes[testno].keylength[hashno] ? + hmachashes[testno].keylength[hashno] : + hmachashes[testno].keylength[1] ? + hmachashes[testno].keylength[1] : + hmachashes[testno].keylength[0], + 0, 0, 0, + hmachashes[testno].resultarray[hashno], + hmachashes[testno].resultlength[hashno], + printResults, printPassFail); + } else if (runHkdfTests) { + err = hashHkdf(testno, loopno, hashno, + printResults, printPassFail); + } else { /* sha tests */ + err = hash(testno, loopno, hashno, + hashes[hashno].tests[testno].testarray, + hashes[hashno].tests[testno].length, + hashes[hashno].tests[testno].repeatcount, + hashes[hashno].tests[testno].numberExtrabits, + hashes[hashno].tests[testno].extrabits, + 0, 0, 0, 0, 0, + hashes[hashno].tests[testno].resultarray, + hashes[hashno].hashsize, + printResults, printPassFail); + } + } + if (!runHmacTests && !runHkdfTests) { + randomtest(hashno, hashes[hashno].randomtest, + hashes[hashno].hashsize, hashes[hashno].randomresults, + RANDOMCOUNT, printResults, printPassFail); + } + } + } + } + + + + +Eastlake & Hansen Informational [Page 122] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + /* Test some error returns */ + if (checkErrors) { + testErrors(hashnolow, hashnohigh, printResults, printPassFail); + } + + return 0; +} + +/* + * Compare two strings, case independently. + * Equivalent to strcasecmp() found on some systems. + */ +int scasecmp(const char *s1, const char *s2) +{ + for (;;) { + char u1 = tolower((int)(unsigned char)(*s1++)); + char u2 = tolower((int)(unsigned char)(*s2++)); + if (u1 != u2) + return u1 - u2; + if (u1 == '\0') + return 0; + } +} + +9. Security Considerations + + This document is intended to provide convenient open source access by + the Internet community to the United States of America Federal + Information Processing Standard Secure Hash Algorithms (SHAs) [FIPS + 180-2], HMACs based thereon, and HKDF. No independent assertion of + the security of these functions by the authors for any particular use + is intended. + + See [RFC6194] for a discussion of SHA-1 Security Considerations. + +10. Acknowledgements + + Thanks for the corrections to [RFC4634] that were provided by Alfred + Hoenes and Jan Andres and to Alfred's comments on the document + hereof. + + Also to the following in alphabetic order, whose comments lead to + improvements in the document: James Carlson, Russ Housley, Tero + Kivinen, Juergen Quittek, and Sean Turner. + + + + + + + +Eastlake & Hansen Informational [Page 123] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + +11. References + +11.1. Normative References + + [RFC2104] Krawczyk, H., Bellare, M., and R. Canetti, "HMAC: Keyed- + Hashing for Message Authentication", RFC 2104, February + 1997. + + [RFC5869] Krawczyk, H. and P. Eronen, "HMAC-based Extract-and-Expand + Key Derivation Function (HKDF)", RFC 5869, May 2010. + + [SHS] "Secure Hash Standard", United States of American, + National Institute of Science and Technology, Federal + Information Processing Standard (FIPS) 180-3, + http://csrc.nist.gov/publications/fips/fips180-3/ + fips180-3_final.pdf. + + [US-ASCII] ANSI, "USA Standard Code for Information Interchange", + X3.4, American National Standards Institute: New York, + 1968. + +11.2. Informative References + + [RFC3174] Eastlake 3rd, D. and P. Jones, "US Secure Hash Algorithm 1 + (SHA1)", RFC 3174, September 2001. + + [RFC3874] Housley, R., "A 224-bit One-way Hash Function: + SHA-224", RFC 3874, September 2004. + + [RFC4055] Schaad, J., Kaliski, B., and R. Housley, "Additional + Algorithms and Identifiers for RSA Cryptography for use in + the Internet X.509 Public Key Infrastructure Certificate + and Certificate Revocation List (CRL) Profile", RFC 4055, + June 2005. + + [RFC4086] Eastlake 3rd, D., Schiller, J., and S. Crocker, + "Randomness Requirements for Security", BCP 106, RFC 4086, + June 2005. + + [RFC4634] Eastlake 3rd, D. and T. Hansen, "US Secure Hash Algorithms + (SHA and HMAC-SHA)", RFC 4634, July 2006. + + [RFC6194] Polk, T., Chen, L., Turner, S., and P. Hoffman, "Security + Considerations for the SHA-0 and SHA-1 Message-Digest + Algorithms", RFC 6194, March 2011. + + + + + + +Eastlake & Hansen Informational [Page 124] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + + [SHAVS] "The Secure Hash Algorithm Validation System (SHAVS)", + http://csrc.nist.gov/groups/STM/cavp/documents/shs/ + SHAVS.pdf, July 2004. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Eastlake & Hansen Informational [Page 125] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + +Appendix: Changes from RFC 4634 + + The following changes were made to RFC 4634 to produce this document: + + 1. Add code for HKDF and brief text about HKDF with pointer to + [RFC5869]. + + 2. Fix numerous errata filed against [RFC4634] as included below. + Note that in no case did the old code return an incorrect hash + value. + + 2.a. Correct some of the error return values which has erroneously + been "shaNull" to the correct "shaInputTooLong" error. + + 2.b. Update comments and variable names within the code for + consistency and clarity and other editorial changes. + + 2.c. The previous code for SHA-384 and SHA-512 would stop after + 2^93 bytes (2^96 bits). The fixed code handles up to 2^125 + bytes (2^128 bits). + + 2.d. Add additional error checking including a run time check in + the test driver to detect attempts to run the test driver + after compilation using some other character set instead of + [US-ASCII]. + + 3. Update boilerplate, remove special license in [RFC4634] as new + boilerplate mandates simplified BSD license. + + 4. Replace MIT version of getopt with new code to satisfy IETF + incoming and outgoing license restrictions. + + 5. Add references to [RFC6194]. + + 6. Other assorted editorial improvements. + + + + + + + + + + + + + + + + +Eastlake & Hansen Informational [Page 126] + +RFC 6234 SHAs, HMAC-SHAs, and HKDF May 2011 + + +Author's Address + + Donald Eastlake + Huawei + 155 Beaver Street + Milford, MA 01757 USA + + Telephone: +1-508-333-2270 + EMail: d3e3e3@gmail.com + + + Tony Hansen + AT&T Laboratories + 200 Laurel Ave. + Middletown, NJ 07748 USA + + Telephone: +1-732-420-8934 + EMail: tony+shs@maillennium.att.com + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Eastlake & Hansen Informational [Page 127] + diff --git a/src/common/libManageSieve/tests/SieveBase64Test.mjs b/src/common/libManageSieve/tests/SieveBase64Test.mjs new file mode 100644 index 00000000..ee1a61a5 --- /dev/null +++ b/src/common/libManageSieve/tests/SieveBase64Test.mjs @@ -0,0 +1,273 @@ +/* + * The contents of this file are licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + * + */ + +/* eslint-disable no-magic-numbers */ + +/* global net */ +const suite = net.tschmid.yautt.test; + +if (!suite) + throw new Error("Could not initialize test suite"); + +import { + SieveBase64Decoder, + SieveBase64Encoder +} from "./../SieveBase64.mjs"; + + +// Test vectors from RFC +suite.add("Base64 Decoder - Empty String", function () { + suite.assertArrayEquals("", + (new SieveBase64Decoder("")).toArray()); + + suite.assertEquals("", + (new SieveBase64Decoder("")).toUtf8()); +}); + +suite.add("Base64 Decoder - Zg==", function () { + suite.assertArrayEquals("f", + (new SieveBase64Decoder("Zg==")).toArray()); + + suite.assertEquals("f", + (new SieveBase64Decoder("Zg==")).toUtf8()); +}); + +suite.add("Base64 Decoder - Zm8=", function () { + suite.assertArrayEquals("fo", + (new SieveBase64Decoder("Zm8=")).toArray()); + + suite.assertEquals("fo", + (new SieveBase64Decoder("Zm8=")).toUtf8()); +}); + +suite.add("Base64 Decoder - Zm9v", function () { + suite.assertArrayEquals("foo", + (new SieveBase64Decoder("Zm9v")).toArray()); + + suite.assertEquals("foo", + (new SieveBase64Decoder("Zm9v")).toUtf8()); +}); + +suite.add("Base64 Decoder - Zm9vYg==", function () { + suite.assertArrayEquals("foob", + (new SieveBase64Decoder("Zm9vYg==")).toArray()); + + suite.assertEquals("foob", + (new SieveBase64Decoder("Zm9vYg==")).toUtf8()); +}); + +suite.add("Base64 Decoder - Zm9vYmE=", function () { + suite.assertArrayEquals("fooba", + (new SieveBase64Decoder("Zm9vYmE=")).toArray()); + + suite.assertEquals("fooba", + (new SieveBase64Decoder("Zm9vYmE=")).toUtf8()); +}); + +suite.add("Base64 Decoder - Zm9vYmFy", function () { + suite.assertArrayEquals("foobar", + (new SieveBase64Decoder("Zm9vYmFy")).toArray()); + + suite.assertEquals("foobar", + (new SieveBase64Decoder("Zm9vYmFy")).toUtf8()); +}); + +// Test vectors from Wikipedia +suite.add("Base64 Decoder - YW55IGNhcm5hbCBwbGVhcw", function () { + suite.assertArrayEquals("any carnal pleas", + (new SieveBase64Decoder("YW55IGNhcm5hbCBwbGVhcw")).toArray()); + + suite.assertEquals("any carnal pleas", + (new SieveBase64Decoder("YW55IGNhcm5hbCBwbGVhcw")).toUtf8()); +}); + +suite.add("Base64 Decoder - YW55IGNhcm5hbCBwbGVhcw==", function () { + suite.assertArrayEquals("any carnal pleas", + (new SieveBase64Decoder("YW55IGNhcm5hbCBwbGVhcw==")).toArray()); + + suite.assertEquals("any carnal pleas", + (new SieveBase64Decoder("YW55IGNhcm5hbCBwbGVhcw==")).toUtf8()); +}); + +suite.add("Base64 Decoder - YW55IGNhcm5hbCBwbGVhc3U", function () { + suite.assertArrayEquals("any carnal pleasu", + (new SieveBase64Decoder("YW55IGNhcm5hbCBwbGVhc3U")).toArray()); + + suite.assertEquals("any carnal pleasu", + (new SieveBase64Decoder("YW55IGNhcm5hbCBwbGVhc3U")).toUtf8()); +}); + +suite.add("Base64 Decoder - YW55IGNhcm5hbCBwbGVhc3U=", function () { + suite.assertArrayEquals("any carnal pleasu", + (new SieveBase64Decoder("YW55IGNhcm5hbCBwbGVhc3U=")).toArray()); + + suite.assertEquals("any carnal pleasu", + (new SieveBase64Decoder("YW55IGNhcm5hbCBwbGVhc3U=")).toUtf8()); +}); + +suite.add("Base64 Decoder - YW55IGNhcm5hbCBwbGVhc3Vy", function () { + suite.assertArrayEquals("any carnal pleasur", + (new SieveBase64Decoder("YW55IGNhcm5hbCBwbGVhc3Vy")).toArray()); + + suite.assertEquals("any carnal pleasur", + (new SieveBase64Decoder("YW55IGNhcm5hbCBwbGVhc3Vy")).toUtf8()); +}); + +suite.add("Base64 Decoder - YW55IGNhcm5hbCBwbGVhc3VyZQ==", function () { + suite.assertArrayEquals("any carnal pleasure", + (new SieveBase64Decoder("YW55IGNhcm5hbCBwbGVhc3VyZQ==")).toArray()); + + suite.assertEquals("any carnal pleasure", + (new SieveBase64Decoder("YW55IGNhcm5hbCBwbGVhc3VyZQ==")).toUtf8()); +}); + +suite.add("Base64 Decoder - dj1wZVI4RWI2dU5mNFpkN0h6SjB2N09CZy9DNWN4cEtWdU1SeDNsY25hT2hFPQ=", function () { + suite.assertArrayEquals("v=peR8Eb6uNf4Zd7HzJ0v7OBg/C5cxpKVuMRx3lcnaOhE=", + (new SieveBase64Decoder("dj1wZVI4RWI2dU5mNFpkN0h6SjB2N09CZy9DNWN4cEtWdU1SeDNsY25hT2hFPQ=")).toArray()); + + suite.assertEquals("v=peR8Eb6uNf4Zd7HzJ0v7OBg/C5cxpKVuMRx3lcnaOhE=", + (new SieveBase64Decoder("dj1wZVI4RWI2dU5mNFpkN0h6SjB2N09CZy9DNWN4cEtWdU1SeDNsY25hT2hFPQ=")).toUtf8()); +}); + +suite.add("Base64 Decoder - peR8Eb6uNf4Zd7HzJ0v7OBg/C5cxpKVuMRx3lcnaOhE=", function () { + + const expectation = new Uint8Array([0xA5, 0xE4, 0x7C, 0x11, 0xBE, 0xAE, 0x35, 0xFE, 0x19, + 0x77, 0xB1, 0xF3, 0x27, 0x4B, 0xFB, 0x38, 0x18, 0x3F, 0x0B, 0x97, 0x31, + 0xA4, 0xA5, 0x6E, 0x31, 0x1C, 0x77, 0x95, 0xC9, 0xDA, 0x3A, 0x11]); + + suite.assertArrayEquals(expectation, + (new SieveBase64Decoder("peR8Eb6uNf4Zd7HzJ0v7OBg/C5cxpKVuMRx3lcnaOhE=")).toArray()); +}); + +suite.add("Base64 Decoder - peR8Eb6uNf4Zd7HzJ0v7OBg/C5cxpKVuMRx3lcnaOhE", function () { + + const expectation = new Uint8Array([0xA5, 0xE4, 0x7C, 0x11, 0xBE, 0xAE, 0x35, 0xFE, 0x19, + 0x77, 0xB1, 0xF3, 0x27, 0x4B, 0xFB, 0x38, 0x18, 0x3F, 0x0B, 0x97, 0x31, + 0xA4, 0xA5, 0x6E, 0x31, 0x1C, 0x77, 0x95, 0xC9, 0xDA, 0x3A, 0x11]); + + suite.assertArrayEquals(expectation, + (new SieveBase64Decoder("peR8Eb6uNf4Zd7HzJ0v7OBg/C5cxpKVuMRx3lcnaOhE")).toArray()); +}); + +// Test vectors from RFC +suite.add("Base64 Encoder - Empty String", function () { + suite.assertArrayEquals("", + (new SieveBase64Encoder("")).toArray()); + + suite.assertEquals("", + (new SieveBase64Encoder("")).toUtf8()); +}); + +suite.add("Base64 Encoder - Zg==", function () { + suite.assertArrayEquals("Zg==", + (new SieveBase64Encoder("f")).toArray()); + + suite.assertEquals("Zg==", + (new SieveBase64Encoder("f")).toUtf8()); +}); + +suite.add("Base64 Encoder - Zm8=", function () { + suite.assertArrayEquals("Zm8=", + (new SieveBase64Encoder("fo")).toArray()); + + suite.assertEquals("Zm8=", + (new SieveBase64Encoder("fo")).toUtf8()); +}); + +suite.add("Base64 Encoder - Zm9v", function () { + suite.assertArrayEquals("Zm9v", + (new SieveBase64Encoder("foo")).toArray()); + + suite.assertEquals("Zm9v", + (new SieveBase64Encoder("foo")).toUtf8()); +}); + +suite.add("Base64 Encoder - Zm9vYg==", function () { + suite.assertArrayEquals("Zm9vYg==", + (new SieveBase64Encoder("foob")).toArray()); + + suite.assertEquals("Zm9vYg==", + (new SieveBase64Encoder("foob")).toUtf8()); +}); + +suite.add("Base64 Encoder - Zm9vYmE=", function () { + suite.assertArrayEquals("Zm9vYmE=", + (new SieveBase64Encoder("fooba")).toArray() ); + + suite.assertEquals("Zm9vYmE=", + (new SieveBase64Encoder("fooba")).toUtf8() ); +}); + +suite.add("Base64 Encoder - Zm9vYmFy", function () { + suite.assertArrayEquals("Zm9vYmFy", + (new SieveBase64Encoder("foobar")).toArray()); + + suite.assertEquals("Zm9vYmFy", + (new SieveBase64Encoder("foobar")).toUtf8()); +}); + + +// Test vectors from Wikipedia + +suite.add("Base64 Encoder - YW55IGNhcm5hbCBwbGVhcw==", function () { + suite.assertArrayEquals("YW55IGNhcm5hbCBwbGVhcw==", + (new SieveBase64Encoder("any carnal pleas")).toArray()); + + suite.assertEquals("YW55IGNhcm5hbCBwbGVhcw==", + (new SieveBase64Encoder("any carnal pleas")).toUtf8()); +}); + +suite.add("Base64 Encoder - YW55IGNhcm5hbCBwbGVhc3U=", function () { + suite.assertArrayEquals("YW55IGNhcm5hbCBwbGVhc3U=", + (new SieveBase64Encoder("any carnal pleasu")).toArray()); + + suite.assertEquals("YW55IGNhcm5hbCBwbGVhc3U=", + (new SieveBase64Encoder("any carnal pleasu")).toUtf8()); +}); + +suite.add("Base64 Encoder - YW55IGNhcm5hbCBwbGVhc3Vy", function () { + suite.assertArrayEquals("YW55IGNhcm5hbCBwbGVhc3Vy", + (new SieveBase64Encoder("any carnal pleasur")).toArray()); + + suite.assertEquals("YW55IGNhcm5hbCBwbGVhc3Vy", + (new SieveBase64Encoder("any carnal pleasur")).toUtf8()); +}); + +suite.add("Base64 Encoder - YW55IGNhcm5hbCBwbGVhc3VyZQ==", function () { + suite.assertArrayEquals("YW55IGNhcm5hbCBwbGVhc3VyZQ==", + (new SieveBase64Encoder("any carnal pleasure")).toArray()); + + suite.assertEquals("YW55IGNhcm5hbCBwbGVhc3VyZQ==", + (new SieveBase64Encoder("any carnal pleasure")).toUtf8()); +}); + +suite.add("Base64 Encoder - dj1wZVI4RWI2dU5mNFpkN0h6SjB2N09CZy9DNWN4cEtWdU1SeDNsY25hT2hFPQ==", function () { + suite.assertArrayEquals("dj1wZVI4RWI2dU5mNFpkN0h6SjB2N09CZy9DNWN4cEtWdU1SeDNsY25hT2hFPQ==", + (new SieveBase64Encoder("v=peR8Eb6uNf4Zd7HzJ0v7OBg/C5cxpKVuMRx3lcnaOhE=")).toArray()); + + suite.assertEquals("dj1wZVI4RWI2dU5mNFpkN0h6SjB2N09CZy9DNWN4cEtWdU1SeDNsY25hT2hFPQ==", + (new SieveBase64Encoder("v=peR8Eb6uNf4Zd7HzJ0v7OBg/C5cxpKVuMRx3lcnaOhE=")).toUtf8()); +}); + +suite.add("Base64 Encoder - peR8Eb6uNf4Zd7HzJ0v7OBg/C5cxpKVuMRx3lcnaOhE=", function () { + + const actual = new Uint8Array([0xA5, 0xE4, 0x7C, 0x11, 0xBE, 0xAE, 0x35, 0xFE, 0x19, + 0x77, 0xB1, 0xF3, 0x27, 0x4B, 0xFB, 0x38, 0x18, 0x3F, 0x0B, 0x97, 0x31, + 0xA4, 0xA5, 0x6E, 0x31, 0x1C, 0x77, 0x95, 0xC9, 0xDA, 0x3A, 0x11]); + + suite.assertArrayEquals("peR8Eb6uNf4Zd7HzJ0v7OBg/C5cxpKVuMRx3lcnaOhE=", + (new SieveBase64Encoder(actual)).toArray()); + + suite.assertEquals("peR8Eb6uNf4Zd7HzJ0v7OBg/C5cxpKVuMRx3lcnaOhE=", + (new SieveBase64Encoder(actual)).toUtf8()); +}); + diff --git a/src/common/libManageSieve/tests/SieveCryptoTest.mjs b/src/common/libManageSieve/tests/SieveCryptoTest.mjs new file mode 100644 index 00000000..678b4c82 --- /dev/null +++ b/src/common/libManageSieve/tests/SieveCryptoTest.mjs @@ -0,0 +1,50 @@ +/* + * The contents of this file are licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + * + */ + +/* eslint-disable no-magic-numbers */ + +/* global net */ +const suite = net.tschmid.yautt.test; + +if (!suite) + throw new Error("Could not initialize test suite"); + +import { SieveCrypto } from "./../SieveCrypto.mjs"; + +const DEFAULT_PASSWORD = new Uint8Array([112, 101, 110, 99, 105, 108]); +const DEFAULT_SALT = new Uint8Array([65, 37, 194, 71, 228, 58, 177, 233, 60, 109, 255, 118]); + +const DEFAULT_SALTED_PASSWORD = [ + 29, 150, 238, 58, 82, 155, 90, 95, 158, 71, + 192, 31, 34, 154, 44, 184, 166, 225, 95, 125]; + +suite.description( + "Testing Cryptographic functions..."); + +suite.add("Crypto SHA1 Hi()", async function () { + + const crypto = new SieveCrypto("SHA-1"); + + const password = DEFAULT_PASSWORD; + const iter = "4096"; + const salt = DEFAULT_SALT; + + + const saltedPassword = await (crypto.Hi(password, salt, iter)); + + suite.assertEquals(20, saltedPassword.length); + + suite.assertEquals(saltedPassword.toString(), DEFAULT_SALTED_PASSWORD.toString()); +}); + +suite.add("Normalize(str)", function () { +}); diff --git a/src/common/libManageSieve/tests/SieveSaslRequestTest.mjs b/src/common/libManageSieve/tests/SieveSaslRequestTest.mjs new file mode 100644 index 00000000..5e4a5990 --- /dev/null +++ b/src/common/libManageSieve/tests/SieveSaslRequestTest.mjs @@ -0,0 +1,456 @@ +/* + * The contents of this file are licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + * + */ + +/* global net */ +const suite = net.tschmid.yautt.test; + +if (!suite) + throw new Error("Could not initialize test suite"); + +import { + SieveSaslPlainRequest, + SieveSaslScramSha1Request, + SieveSaslScramSha256Request, + SieveSaslExternalRequest, + SieveSaslLoginRequest +} from "./../SieveRequest.mjs"; + +import { SieveRequestBuilder } from "./../SieveRequestBuilder.mjs"; +import { SieveResponseParser } from "./../SieveResponseParser.mjs"; + +const SIMPLE_PASSWORD = "pencil"; +const COMPLEX_PASSWORD = "abc§123"; +const INSANE_PASSWORD = "f>¤¨&ú/N¸Ýì_`ÛÄ*gÅß]Ö¯Xq¼/±Æ_û.Q*¤ú½kat©z×\\\\®DèÍñ(_d©.Éê³BSv÷{fÊÚõp·ÅähBÏ)YÕý=ýtZ+í(a'8¶Y´HV(m´ûÂ$äK2]*ûöìµ.+^µÈ6ÛðÄ/ÝÉÐo¡%+49"; + + +suite.description( + "Testing Sasl Mechanisms..."); + +/** + * Simulates a request. + * + * @param {SieveAbstractRequest} request + * the request which should be used. + * @param {boolean} completed + * if true the request is completed after this call. + * @param {string} expectation + * the expected request + */ +async function expectRequest(request, completed, expectation) { + + suite.assertEquals(request.hasNextRequest(), !completed); + + const requestBuilder = new SieveRequestBuilder(); + await request.getNextRequest(requestBuilder); + + suite.assertEquals(requestBuilder.getBytes(), expectation); +} + +/** + * Simulates a response + * + * @param {SieveAbstractRequest} request + * the request which expects the response. + * @param {string} expectation + * the requests expected response. + */ +async function expectResponse(request, expectation) { + expectation = Array.from(expectation, (x) => { return x.charCodeAt(0);} ); + await request.onResponse(new SieveResponseParser(expectation)); +} + +suite.add("SASL Plain", async function() { + const request = new SieveSaslPlainRequest(); + + suite.assertTrue(request.isAuthorizable()); + suite.assertTrue(request.hasPassword()); + + request.setUsername("user"); + request.setPassword(SIMPLE_PASSWORD); + + await expectRequest(request, true, + `AUTHENTICATE "PLAIN" "AHVzZXIAcGVuY2ls"\r\n`); + + await expectResponse(request, + `OK "Logged in."\r\n`); + + suite.assertFalse(request.hasNextRequest()); +}); + +suite.add("SASL Plain with special Characters", async function() { + const request = new SieveSaslPlainRequest(); + + suite.assertTrue(request.isAuthorizable()); + suite.assertTrue(request.hasPassword()); + + request.setUsername("user2"); + request.setPassword(COMPLEX_PASSWORD); + + await expectRequest(request, true, + `AUTHENTICATE "PLAIN" "AHVzZXIyAGFiY8KnMTIz"\r\n`); + + await expectResponse(request, + `OK "Logged in."\r\n`); + + suite.assertFalse(request.hasNextRequest()); +}); + +suite.add("SASL Plain with many Characters", async function() { + const request = new SieveSaslPlainRequest(); + + suite.assertTrue(request.isAuthorizable()); + suite.assertTrue(request.hasPassword()); + + request.setUsername("user3"); + request.setPassword(INSANE_PASSWORD); + + await expectRequest(request, true, + `AUTHENTICATE "PLAIN" "AHVzZXIzAGY+wqTCqCbDui9OwrjDncOsX2DDm8OEKmfDhcOfXcOWwq9YccK8L8Kxw4Zfw7suUSrCpMO6wr1rYXTCqXrDl1xcwq5Ew6jDjcOxKF9kwqkuw4nDqsKzQlN2w7d7ZsOKw5rDtXDCt8OFw6RoQsOPKVnDlcO9PcO9dForw60oYSc4wrZZwrRIVihtwrTDu8OCJMOkSzJdKsO7w7bDrMK1LitewrXDiDbDm8Oww4Qvw53DicOQb8KhJSs0OQ=="\r\n`); + + await expectResponse(request, + `OK "Logged in."\r\n`); + + suite.assertFalse(request.hasNextRequest()); +}); + + +suite.add("SASL Scram SHA1 - Short", async function () { + const request = new SieveSaslScramSha1Request(); + + suite.assertEquals(request.getSaslName(), "SCRAM-SHA-1"); + suite.assertTrue(request.isAuthorizable()); + suite.assertTrue(request.hasPassword()); + + request.setUsername("user"); + request.setPassword(SIMPLE_PASSWORD); + + request.generateNonce = async () => { return "fyko+d2lbbFgONRv9qkxdawL"; }; + + // C: n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL + await expectRequest(request, false, + `AUTHENTICATE "SCRAM-SHA-1" "biwsbj11c2VyLHI9ZnlrbytkMmxiYkZnT05Sdjlxa3hkYXdM"\r\n`); + + // S: r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096 + await expectResponse(request, + `"cj1meWtvK2QybGJiRmdPTlJ2OXFreGRhd0wzcmZjTkhZSlkxWlZ2V1ZzN2oscz1RU1hDUitRNnNlazhiZjkyLGk9NDA5Ng=="\r\n`); + + // C: c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts= + await expectRequest(request, false, + `"Yz1iaXdzLHI9ZnlrbytkMmxiYkZnT05Sdjlxa3hkYXdMM3JmY05IWUpZMVpWdldWczdqLHA9djBYOHYzQnoyVDBDSkdiSlF5RjBYK0hJNFRzPQ=="\r\n`); + + // S: v=rmF9pqV8S7suAoZWja4dJRkFsKQ= + await expectResponse(request, + `OK (SASL "dj1ybUY5cHFWOFM3c3VBb1pXamE0ZEpSa0ZzS1E9")\r\n`); + + suite.assertFalse(request.hasNextRequest()); +}); + +suite.add("SASL Scram SHA1 - Long", async function () { + const request = new SieveSaslScramSha1Request(); + + suite.assertEquals(request.getSaslName(), "SCRAM-SHA-1"); + suite.assertTrue(request.isAuthorizable()); + suite.assertTrue(request.hasPassword()); + + request.setUsername("user"); + request.setPassword(SIMPLE_PASSWORD); + + request.generateNonce = async () => { return "fyko+d2lbbFgONRv9qkxdawL"; }; + + // C: n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL + await expectRequest(request, false, + `AUTHENTICATE "SCRAM-SHA-1" "biwsbj11c2VyLHI9ZnlrbytkMmxiYkZnT05Sdjlxa3hkYXdM"\r\n`); + + // S: r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096 + await expectResponse(request, + `"cj1meWtvK2QybGJiRmdPTlJ2OXFreGRhd0wzcmZjTkhZSlkxWlZ2V1ZzN2oscz1RU1hDUitRNnNlazhiZjkyLGk9NDA5Ng=="\r\n`); + + // C: c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts= + await expectRequest(request, false, + `"Yz1iaXdzLHI9ZnlrbytkMmxiYkZnT05Sdjlxa3hkYXdMM3JmY05IWUpZMVpWdldWczdqLHA9djBYOHYzQnoyVDBDSkdiSlF5RjBYK0hJNFRzPQ=="\r\n`); + + // S: v=rmF9pqV8S7suAoZWja4dJRkFsKQ= + await expectResponse(request, + `"dj1ybUY5cHFWOFM3c3VBb1pXamE0ZEpSa0ZzS1E9"\r\n`); + + await expectRequest(request, false, + `""\r\n`); + + await expectResponse(request, + `OK\r\n`); + + suite.assertFalse(request.hasNextRequest()); +}); + +suite.add("SASL Scram SHA1 with Special Characters - Long", async function () { + const request = new SieveSaslScramSha1Request(); + + suite.assertEquals(request.getSaslName(), "SCRAM-SHA-1"); + suite.assertTrue(request.isAuthorizable()); + suite.assertTrue(request.hasPassword()); + + request.setUsername("user2"); + request.setPassword(COMPLEX_PASSWORD); + + request.generateNonce = async () => { return "4Gn+oXMVuHyu9RVYooRFMw+x"; }; + + // n,,n=user2,r=6cb260fdd390fcb04ae5bd1edf7d25d207db01c9 + // C: n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL + await expectRequest(request, false, + `AUTHENTICATE "SCRAM-SHA-1" "biwsbj11c2VyMixyPTRHbitvWE1WdUh5dTlSVllvb1JGTXcreA=="\r\n`); + + // S: r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096 + await expectResponse(request, + `"cj00R24rb1hNVnVIeXU5UlZZb29SRk13K3hqJy0kZ0g5aXVjRGBDS1dEOCNOWT46TzF2NnI2I25VKDVXSyFEY3ROaj4/Zn40Y0k0QSMiVUB3TjMoMjtdZls/LHM9UUR2V2hqUTJEYjVVUFJ0RFZaYWROZz09LGk9NDA5Ng=="\r\n`); + + // c=biws,r=6cb260fdd390fcb04ae5bd1edf7d25d207db01c9'Uv)RBWr>8Y~8NktH`Y9A$Zh!*'a.Q'%;8=!CS'L[W&K`H3LdOc6uV5+nHG.>(x^,p=4w7qjWbJv2IRU+XcjysMxhJYJUI= + // C: c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts= + await expectRequest(request, false, + `"Yz1iaXdzLHI9NEduK29YTVZ1SHl1OVJWWW9vUkZNdyt4aictJGdIOWl1Y0RgQ0tXRDgjTlk+Ok8xdjZyNiNuVSg1V0shRGN0Tmo+P2Z+NGNJNEEjIlVAd04zKDI7XWZbPyxwPXF4T09ndzRDOFJ5Q25LODQ5Y2hIMU95bEpvaz0="\r\n`); + + // S: v=rmF9pqV8S7suAoZWja4dJRkFsKQ= + await expectResponse(request, + `"dj1xem1XN1NSL0FObGVYbnBRSXorckhQOGt4ODA9"\r\n`); + + await expectRequest(request, false, + `""\r\n`); + + await expectResponse(request, + `OK\r\n`); + + suite.assertFalse(request.hasNextRequest()); +}); + +suite.add("SASL Scram SHA1 with many special characters - Long", async function () { + const request = new SieveSaslScramSha1Request(); + + suite.assertEquals(request.getSaslName(), "SCRAM-SHA-1"); + suite.assertTrue(request.isAuthorizable()); + suite.assertTrue(request.hasPassword()); + + request.setUsername("user3"); + request.setPassword(INSANE_PASSWORD); + + request.generateNonce = async () => { return "c96a5d9a095401657971d4ad44c51e0147bf52e3"; }; + + await expectRequest(request, false, + `AUTHENTICATE "SCRAM-SHA-1" "biwsbj11c2VyMyxyPWM5NmE1ZDlhMDk1NDAxNjU3OTcxZDRhZDQ0YzUxZTAxNDdiZjUyZTM="\r\n`); + + await expectResponse(request, + `"cj1jOTZhNWQ5YTA5NTQwMTY1Nzk3MWQ0YWQ0NGM1MWUwMTQ3YmY1MmUzIyQyezZWazlhKmskTiNQUGsrNFd3djZyYmcjITZobCc4VVxwY0Q/bVRFS2BiSV5+RCJsRXNjXmNmY2Eoai40bSxzPTZlRFlQTUhCZmZKMk9mVVdTTUIxTmc9PSxpPTQwOTY="\r\n`); + + await expectRequest(request, false, + `"Yz1iaXdzLHI9Yzk2YTVkOWEwOTU0MDE2NTc5NzFkNGFkNDRjNTFlMDE0N2JmNTJlMyMkMns2Vms5YSprJE4jUFBrKzRXd3Y2cmJnIyE2aGwnOFVccGNEP21URUtgYklefkQibEVzY15jZmNhKGouNG0scD1MeFdVZEJBckVPUXZtRHdXbG5oeXBpMDFJN0U9"\r\n`); + + // S: v=rmF9pqV8S7suAoZWja4dJRkFsKQ= + await expectResponse(request, + `"dj04ZzdKMUZKY3NsMTF1U0o4NG5ma3p1WXBBSXM9"\r\n`); + + await expectRequest(request, false, + `""\r\n`); + + await expectResponse(request, + `OK\r\n`); + + suite.assertFalse(request.hasNextRequest()); +}); + +suite.add("SASL Scram SHA256 - Short", async function () { + const request = new SieveSaslScramSha256Request(); + + suite.assertEquals(request.getSaslName(), "SCRAM-SHA-256"); + suite.assertTrue(request.isAuthorizable()); + suite.assertTrue(request.hasPassword()); + + request.setUsername("user"); + request.setPassword(SIMPLE_PASSWORD); + + request.generateNonce = async () => { return "rOprNGfwEbeRWgbNEkqO"; }; + + // C: n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL + await expectRequest(request, false, + `AUTHENTICATE "SCRAM-SHA-256" "biwsbj11c2VyLHI9ck9wck5HZndFYmVSV2diTkVrcU8="\r\n`); + + // S: r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096 + await expectResponse(request, + `"cj1yT3ByTkdmd0ViZVJXZ2JORWtxTyVodllEcFdVYTJSYVRDQWZ1eEZJbGopaE5sRiRrMCxzPVcyMlphSjBTTlk3c29Fc1VFamI2Z1E9PSxpPTQwOTY="\r\n`); + + // C: c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts= + await expectRequest(request, false, + `"Yz1iaXdzLHI9ck9wck5HZndFYmVSV2diTkVrcU8laHZZRHBXVWEyUmFUQ0FmdXhGSWxqKWhObEYkazAscD1kSHpiWmFwV0lrNGpVaE4rVXRlOXl0YWc5empmTUhnc3FtbWl6N0FuZFZRPQ=="\r\n`); + + // S: v=rmF9pqV8S7suAoZWja4dJRkFsKQ= + await expectResponse(request, + `OK (SASL "dj02cnJpVFJCaTIzV3BSUi93dHVwK21NaFVaVW4vZEI1bkxUSlJzamw5NUc0PQ==")\r\n`); + + suite.assertFalse(request.hasNextRequest()); +}); + + +suite.add("SASL Scram SHA256 - Long", async function () { + const request = new SieveSaslScramSha256Request(); + + suite.assertEquals(request.getSaslName(), "SCRAM-SHA-256"); + suite.assertTrue(request.isAuthorizable()); + suite.assertTrue(request.hasPassword()); + + request.setUsername("user"); + request.setPassword(SIMPLE_PASSWORD); + + request.generateNonce = async () => { return "rOprNGfwEbeRWgbNEkqO"; }; + + // C: n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL + await expectRequest(request, false, + `AUTHENTICATE "SCRAM-SHA-256" "biwsbj11c2VyLHI9ck9wck5HZndFYmVSV2diTkVrcU8="\r\n`); + + // S: r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096 + await expectResponse(request, + `"cj1yT3ByTkdmd0ViZVJXZ2JORWtxTyVodllEcFdVYTJSYVRDQWZ1eEZJbGopaE5sRiRrMCxzPVcyMlphSjBTTlk3c29Fc1VFamI2Z1E9PSxpPTQwOTY="\r\n`); + + // C: c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts= + await expectRequest(request, false, + `"Yz1iaXdzLHI9ck9wck5HZndFYmVSV2diTkVrcU8laHZZRHBXVWEyUmFUQ0FmdXhGSWxqKWhObEYkazAscD1kSHpiWmFwV0lrNGpVaE4rVXRlOXl0YWc5empmTUhnc3FtbWl6N0FuZFZRPQ=="\r\n`); + + // S: v=rmF9pqV8S7suAoZWja4dJRkFsKQ= + await expectResponse(request, + `"dj02cnJpVFJCaTIzV3BSUi93dHVwK21NaFVaVW4vZEI1bkxUSlJzamw5NUc0PQ=="\r\n`); + + await expectRequest(request, false, + `""\r\n`); + + await expectResponse(request, + `OK\r\n`); + + suite.assertFalse(request.hasNextRequest()); +}); + +suite.add("SASL Scram SHA256 with Special Characters - Long", async function () { + const request = new SieveSaslScramSha256Request(); + + suite.assertEquals(request.getSaslName(), "SCRAM-SHA-256"); + suite.assertTrue(request.isAuthorizable()); + suite.assertTrue(request.hasPassword()); + + request.setUsername("user2"); + request.setPassword(COMPLEX_PASSWORD); + + request.generateNonce = async () => { return "uGzHUcuMpP47rPcnmk3+WiMU"; }; + + // C: n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL + await expectRequest(request, false, + `AUTHENTICATE "SCRAM-SHA-256" "biwsbj11c2VyMixyPXVHekhVY3VNcFA0N3JQY25tazMrV2lNVQ=="\r\n`); + + // S: r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096 + await expectResponse(request, + `"cj11R3pIVWN1TXBQNDdyUGNubWszK1dpTVVMVWUzXz9LN1BFKzk0ZUo4cjNPZjplfis7RjIla2k/fUFfPC5zTypuUHxbR2hCUWRcWFgyQmsyLVBdejkkZiNdLHM9RVZodlRpZ2swWTRnNWljTzJQMDZDdz09LGk9NDA5Ng=="\r\n`); + + // C: c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts= + await expectRequest(request, false, + `"Yz1iaXdzLHI9dUd6SFVjdU1wUDQ3clBjbm1rMytXaU1VTFVlM18/SzdQRSs5NGVKOHIzT2Y6ZX4rO0YyJWtpP31BXzwuc08qblB8W0doQlFkXFhYMkJrMi1QXXo5JGYjXSxwPTU4SUE3TWt3OXhBUUxVTGdvc3dVaTNEM1M3QkdMRkpwTVd3N3p0a3gwVjA9"\r\n`); + + // S: v=rmF9pqV8S7suAoZWja4dJRkFsKQ= + await expectResponse(request, + `"dj1wZVI4RWI2dU5mNFpkN0h6SjB2N09CZy9DNWN4cEtWdU1SeDNsY25hT2hFPQ="\r\n`); + + await expectRequest(request, false, + `""\r\n`); + + await expectResponse(request, + `OK\r\n`); + + suite.assertFalse(request.hasNextRequest()); +}); + +suite.add("SASL Scram SHA256 with many special characters - Long", async function () { + const request = new SieveSaslScramSha256Request(); + + suite.assertEquals(request.getSaslName(), "SCRAM-SHA-256"); + suite.assertTrue(request.isAuthorizable()); + suite.assertTrue(request.hasPassword()); + + request.setUsername("user3"); + request.setPassword(INSANE_PASSWORD); + + request.generateNonce = async () => { return "fc88adb75eb0151cfbc116b6cca317343b08b68a1411a889c77d60df9180cf95"; }; + + await expectRequest(request, false, + `AUTHENTICATE "SCRAM-SHA-256" "biwsbj11c2VyMyxyPWZjODhhZGI3NWViMDE1MWNmYmMxMTZiNmNjYTMxNzM0M2IwOGI2OGExNDExYTg4OWM3N2Q2MGRmOTE4MGNmOTU="\r\n`); + + await expectResponse(request, + `"cj1mYzg4YWRiNzVlYjAxNTFjZmJjMTE2YjZjY2EzMTczNDNiMDhiNjhhMTQxMWE4ODljNzdkNjBkZjkxODBjZjk1OEwhdEg3XG1xeSJCXEc+UEU7Py0pTjhTZkdRd1BqbXwrLUBeXl4rcExPcy1meEoyQFRcIUtOVFZRcH54cysqUixzPVBSNU5iSWtPWDMzUHVHMHhhck1NMVE9PSxpPTQwOTY="\r\n`); + + await expectRequest(request, false, + `"Yz1iaXdzLHI9ZmM4OGFkYjc1ZWIwMTUxY2ZiYzExNmI2Y2NhMzE3MzQzYjA4YjY4YTE0MTFhODg5Yzc3ZDYwZGY5MTgwY2Y5NThMIXRIN1xtcXkiQlxHPlBFOz8tKU44U2ZHUXdQam18Ky1AXl5eK3BMT3MtZnhKMkBUXCFLTlRWUXB+eHMrKlIscD1ERUw2RGh0bTV5VDg3QWNZRDRuNHNNbjhwZmlnamF2dGpFRURMZ0ROUHc4PQ=="\r\n`); + + // S: v=rmF9pqV8S7suAoZWja4dJRkFsKQ= + await expectResponse(request, + `"dj1zcndEdFRzWmxQQlVuSFk0Vko4UzVNZ0gxaEx6UU9rS1lVREFoNHdqK0pZPQ=="\r\n`); + + await expectRequest(request, false, + `""\r\n`); + + await expectResponse(request, + `OK\r\n`); + + suite.assertFalse(request.hasNextRequest()); +}); + +suite.add("SASL External", async function() { + const request = new SieveSaslExternalRequest(); + + suite.assertTrue(request.isAuthorizable()); + suite.assertFalse(request.hasPassword()); + + await expectRequest(request, true, + `AUTHENTICATE "EXTERNAL" ""\r\n`); + + await expectResponse(request, + `OK\r\n`); + + suite.assertFalse(request.hasNextRequest()); + +}); + +suite.add("SASL Login", async function() { + const request = new SieveSaslLoginRequest(); + + suite.assertFalse(request.isAuthorizable()); + suite.assertTrue(request.hasPassword()); + + request.setUsername("blubb"); + request.setPassword("bla"); + + await expectRequest(request, false, + 'AUTHENTICATE "LOGIN"\r\n'); + + // Server responds with a "VXNlcm5hbWU6" which is a "USERNAME:" + await expectResponse(request, + '"VXNlcm5hbWU6"\r\n'); + + // Client sends the username, Ymx1YmI= equals blubb + await expectRequest(request, false, + '"Ymx1YmI="\r\n'); + + // Server responds with a "UGFzc3dvcmQ6" which is a "PASSWORD:" + await expectResponse(request, + '"UGFzc3dvcmQ6"\r\n'); + + // Client sends the password, "Ymxh" equals bla + await expectRequest(request, false, + '"Ymxh"\r\n'); + + await expectResponse(request, + `OK\r\n`); + + suite.assertFalse(request.hasNextRequest()); + +}); diff --git a/src/common/libSieve/SieveGui.html b/src/common/libSieve/SieveGui.html index 9ff0f4da..ab469d69 100755 --- a/src/common/libSieve/SieveGui.html +++ b/src/common/libSieve/SieveGui.html @@ -119,7 +119,7 @@ <body> <div class="container-xl mt-2"> <!-- the toolvar --> - <div id="toolbar" class="ml-2"></div> + <div id="toolbar" class="ms-2"></div> <div id="content"> @@ -142,7 +142,7 @@ <ul id="sivDialogTabs" class="nav nav-tabs card-header-tabs my-0 pt-3" role="tablist"> </ul> <div class="align-self-center"> - <button type="button" class=".btn-close" data-dismiss="modal" aria-label="Close"></button> + <button type="button" class=".btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> </div> <div class="modal-body"> @@ -150,7 +150,7 @@ </div> </div> <div class="modal-footer"> - <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> <button id="sivDialogSave" type="button" class="btn btn-primary">Apply</button> </div> </div> @@ -159,7 +159,7 @@ <div id="debug2"></div> - <div id="divOutput" class="card p-2 pr-4"> + <div id="divOutput" class="card p-2 pe-4"> </div> <script type="module" src="SieveGui.mjs"></script> diff --git a/src/common/libSieve/SieveGui.mjs b/src/common/libSieve/SieveGui.mjs index ff5a7f92..a877800e 100644 --- a/src/common/libSieve/SieveGui.mjs +++ b/src/common/libSieve/SieveGui.mjs @@ -81,37 +81,37 @@ function init() { // populate the action section const actions = document.querySelector("#sivActions"); while (actions.firstChild) - actions.removeChild(actions.firstChild); + actions.firstChild.remove(); for (key in SieveLexer.types["action"]) if (SieveLexer.types["action"][key].onCapable(SieveLexer.capabilities())) - actions.appendChild(createMenuItem(key, "sieve/action", docShell)); + actions.append(createMenuItem(key, "sieve/action", docShell)); // populate the test section const tests = document.querySelector("#sivTests"); while (tests.firstChild) - tests.removeChild(tests.firstChild); + tests.firstChild.remove(); for (key in SieveLexer.types["test"]) if (SieveLexer.types["test"][key].onCapable(SieveLexer.capabilities())) if (key !== "test/boolean") - tests.appendChild(createMenuItem(key, "sieve/test", docShell)); + tests.append(createMenuItem(key, "sieve/test", docShell)); // populate the operator section const operators = document.querySelector("#sivOperators"); while (operators.firstChild) - operators.removeChild(operators.firstChild); + operators.firstChild.remove(); for (key in SieveLexer.types["operator"]) if (SieveLexer.types["operator"][key].onCapable(SieveLexer.capabilities())) - operators.appendChild(createMenuItem(key, "sieve/operator", docShell)); + operators.append(createMenuItem(key, "sieve/operator", docShell)); // create the trash bin const trash = document.querySelector("#sivTrash"); while (trash.firstChild) - trash.removeChild(trash.firstChild); + trash.firstChild.remove(); - trash.appendChild( + trash.append( new SieveTrashBoxUI(docShell).html()); } @@ -169,9 +169,9 @@ function setSieveScript(script, capabilities) { const output = document.querySelector(`#divOutput`); while (output.firstChild) - output.removeChild(output.firstChild); + output.firstChild.remove(); - output.appendChild(dom2.html()); + output.append(dom2.html()); } /** @@ -274,19 +274,19 @@ async function main() { const debug = document.querySelector(`#debug2`); while (debug.firstChild) - debug.removeChild(debug.firstChild); + debug.firstChild.remove(); // ... and append the new element - debug.appendChild( + debug.append( (await (new SieveTemplate()).load("./templates/debug.html"))); // Clear any existing left overs... const sidebar = document.querySelector(`#toolbar`); while (sidebar.firstChild) - sidebar.removeChild(sidebar.firstChild); + sidebar.firstChild.remove(); // ... and append the new element - sidebar.appendChild( + sidebar.append( (await (new SieveTemplate()).load("./templates/sidebar.html"))); init(); @@ -300,7 +300,7 @@ async function main() { document.querySelector("#CapabilitiesReset") .addEventListener("click", () => { loadCapabilities(); }); - document.querySelector('a[data-toggle="tab"][href="#debugcapabilities"]') + document.querySelector('a[data-bs-toggle="tab"][href="#debugcapabilities"]') .addEventListener('show.bs.tab', function () { loadCapabilities(); }); diff --git a/src/common/libSieve/extensions/RFC5228/logic/SieveConditions.mjs b/src/common/libSieve/extensions/RFC5228/logic/SieveConditions.mjs index 5d24eb91..47c85131 100644 --- a/src/common/libSieve/extensions/RFC5228/logic/SieveConditions.mjs +++ b/src/common/libSieve/extensions/RFC5228/logic/SieveConditions.mjs @@ -154,6 +154,8 @@ class SieveIf extends SieveBlock { * @returns {SieveAbstractElement} */ removeChild(childId, cascade, stop) { + + // eslint-disable-next-line unicorn/prefer-node-remove const elm = super.removeChild(childId); if (cascade && elm) return this; diff --git a/src/common/libSieve/extensions/RFC5228/logic/SieveMatchTypes.mjs b/src/common/libSieve/extensions/RFC5228/logic/SieveMatchTypes.mjs index b6a6b20d..9e573742 100644 --- a/src/common/libSieve/extensions/RFC5228/logic/SieveMatchTypes.mjs +++ b/src/common/libSieve/extensions/RFC5228/logic/SieveMatchTypes.mjs @@ -12,7 +12,7 @@ import { SieveGrammar } from "./../../../toolkit/logic/GenericElements.mjs"; -// TODO match-type items (matchtype/) should not eat tailing whitespaces... +// TODO match-type items (matchtype/) should not eat trailing whitespaces... // they this should be done my the match-type group SieveGrammar.addTag({ diff --git a/src/common/libSieve/extensions/RFC5228/logic/SieveNumbers.mjs b/src/common/libSieve/extensions/RFC5228/logic/SieveNumbers.mjs index 80694993..f57c74f4 100644 --- a/src/common/libSieve/extensions/RFC5228/logic/SieveNumbers.mjs +++ b/src/common/libSieve/extensions/RFC5228/logic/SieveNumbers.mjs @@ -84,9 +84,9 @@ class SieveNumber extends SieveAbstractElement { if (typeof (number) === "undefined" || number === null) throw new Error("Invalid Number"); - number = parseInt(number, 10); + number = Number.parseInt(number, 10); - if (isNaN(number)) + if (Number.isNaN(number)) throw new Error("Not a number: " + number); this._number = number; diff --git a/src/common/libSieve/extensions/RFC5228/logic/SieveOperators.mjs b/src/common/libSieve/extensions/RFC5228/logic/SieveOperators.mjs index 130ffec7..55aae6f0 100644 --- a/src/common/libSieve/extensions/RFC5228/logic/SieveOperators.mjs +++ b/src/common/libSieve/extensions/RFC5228/logic/SieveOperators.mjs @@ -202,8 +202,10 @@ SieveAnyOfAllOfTest.prototype.test // Release old test... this.append(item, old); - if (typeof (old) !== "undefined") + if (typeof (old) !== "undefined") { + // eslint-disable-next-line unicorn/prefer-node-remove this.removeChild(old.id()); + } /* if (this._test) this._test.parent(null); diff --git a/src/common/libSieve/extensions/RFC5228/logic/SieveStrings.mjs b/src/common/libSieve/extensions/RFC5228/logic/SieveStrings.mjs index e94560a1..64188670 100644 --- a/src/common/libSieve/extensions/RFC5228/logic/SieveStrings.mjs +++ b/src/common/libSieve/extensions/RFC5228/logic/SieveStrings.mjs @@ -86,7 +86,7 @@ class SieveMultiLineString extends SieveAbstractElement { this.text = parser.extractUntil("\r\n.\r\n"); // dump the first linebreak and remove dot stuffing - this.text = this.text.substr("\r\n".length).replace(/^\.\./mg, "."); + this.text = this.text.substr("\r\n".length).replace(/^\.\./gm, "."); return this; } @@ -117,7 +117,7 @@ class SieveMultiLineString extends SieveAbstractElement { text += "\r\n"; // Dot stuffing... - text = text.replace(/^\./mg, ".."); + text = text.replace(/^\./gm, ".."); return "text:" + this.whiteSpace.toScript() diff --git a/src/common/libSieve/extensions/RFC5228/templates/SieveAddressTestUI.html b/src/common/libSieve/extensions/RFC5228/templates/SieveAddressTestUI.html index 06a2d2ba..976a3110 100644 --- a/src/common/libSieve/extensions/RFC5228/templates/SieveAddressTestUI.html +++ b/src/common/libSieve/extensions/RFC5228/templates/SieveAddressTestUI.html @@ -1,13 +1,13 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="address.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-addresstest" role="tab"></a> + <a data-i18n="address.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-addresstest" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="address.tab.advanced" class="nav-link" data-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> + <a data-i18n="address.tab.advanced" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="address.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="address.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> @@ -22,7 +22,7 @@ </div> <div id="sivAddressHeaderListTemplate" class="d-none"> - <div class="dropdown-menu dropdown-menu-right"> + <div class="dropdown-menu dropdown-menu-end"> <button class="dropdown-item" type="button">To</button> <button class="dropdown-item" type="button">From</button> <button class="dropdown-item" type="button">Cc</button> diff --git a/src/common/libSieve/extensions/RFC5228/templates/SieveAllOfAnyOfOperator.html b/src/common/libSieve/extensions/RFC5228/templates/SieveAllOfAnyOfOperator.html index 1edf6bb0..cc5aa0c2 100644 --- a/src/common/libSieve/extensions/RFC5228/templates/SieveAllOfAnyOfOperator.html +++ b/src/common/libSieve/extensions/RFC5228/templates/SieveAllOfAnyOfOperator.html @@ -1,10 +1,10 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="allofanyof.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-allofanyof" role="tab"></a> + <a data-i18n="allofanyof.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-allofanyof" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="allofanyof.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="allofanyof.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> diff --git a/src/common/libSieve/extensions/RFC5228/templates/SieveBooleanTest.html b/src/common/libSieve/extensions/RFC5228/templates/SieveBooleanTest.html index 9c8af7ea..2c64534b 100644 --- a/src/common/libSieve/extensions/RFC5228/templates/SieveBooleanTest.html +++ b/src/common/libSieve/extensions/RFC5228/templates/SieveBooleanTest.html @@ -1,11 +1,11 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="boolean.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-test" + <a data-i18n="boolean.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-test" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="boolean.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="boolean.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> diff --git a/src/common/libSieve/extensions/RFC5228/templates/SieveDiscardActionUI.html b/src/common/libSieve/extensions/RFC5228/templates/SieveDiscardActionUI.html index fb889c92..c52f580f 100644 --- a/src/common/libSieve/extensions/RFC5228/templates/SieveDiscardActionUI.html +++ b/src/common/libSieve/extensions/RFC5228/templates/SieveDiscardActionUI.html @@ -1,7 +1,7 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a class="nav-link active" data-toggle="tab" href="#sieve-widget-help" data-i18n="discard.tab.help" role="tab"></a> + <a class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-help" data-i18n="discard.tab.help" role="tab"></a> </li> </ul> diff --git a/src/common/libSieve/extensions/RFC5228/templates/SieveEnvelopeTestUI.html b/src/common/libSieve/extensions/RFC5228/templates/SieveEnvelopeTestUI.html index 6f7d34e8..2ae9b1ac 100644 --- a/src/common/libSieve/extensions/RFC5228/templates/SieveEnvelopeTestUI.html +++ b/src/common/libSieve/extensions/RFC5228/templates/SieveEnvelopeTestUI.html @@ -1,13 +1,13 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="envelope.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-envelope-test" role="tab"></a> + <a data-i18n="envelope.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-envelope-test" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="envelope.tab.advanced" class="nav-link" data-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> + <a data-i18n="envelope.tab.advanced" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="envelope.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="envelope.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> @@ -22,7 +22,7 @@ </div> <div id="sivEnvelopeListTemplate" class="d-none"> - <div class="dropdown-menu dropdown-menu-right"> + <div class="dropdown-menu dropdown-menu-end"> <button class="dropdown-item" type="button">From</button> <button class="dropdown-item" type="button">To</button> </div> diff --git a/src/common/libSieve/extensions/RFC5228/templates/SieveExistsTestUI.html b/src/common/libSieve/extensions/RFC5228/templates/SieveExistsTestUI.html index 34868778..2dd5d2a0 100644 --- a/src/common/libSieve/extensions/RFC5228/templates/SieveExistsTestUI.html +++ b/src/common/libSieve/extensions/RFC5228/templates/SieveExistsTestUI.html @@ -1,11 +1,11 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a class="nav-link active" data-i18n="exists.tab.home" data-toggle="tab" href="#sieve-widget-exists-test" + <a class="nav-link active" data-i18n="exists.tab.home" data-bs-toggle="tab" href="#sieve-widget-exists-test" role="tab"></a> </li> <li class="nav-item"> - <a class="nav-link" data-i18n="exists.tab.help" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a class="nav-link" data-i18n="exists.tab.help" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> @@ -18,7 +18,7 @@ </div> <div id="sivExistsHeaderListTemplate" class="d-none"> - <div class="dropdown-menu dropdown-menu-right"> + <div class="dropdown-menu dropdown-menu-end"> <button class="dropdown-item" type="button">To</button> <button class="dropdown-item" type="button">From</button> <button class="dropdown-item" type="button">Cc</button> diff --git a/src/common/libSieve/extensions/RFC5228/templates/SieveFileIntoActionUI.html b/src/common/libSieve/extensions/RFC5228/templates/SieveFileIntoActionUI.html index 1c0f7b7b..111297c9 100644 --- a/src/common/libSieve/extensions/RFC5228/templates/SieveFileIntoActionUI.html +++ b/src/common/libSieve/extensions/RFC5228/templates/SieveFileIntoActionUI.html @@ -1,10 +1,10 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a class="nav-link active" data-toggle="tab" href="#sieve-widget-fileinto" data-i18n="fileinto.tab.home" role="tab"></a> + <a class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-fileinto" data-i18n="fileinto.tab.home" role="tab"></a> </li> <li class="nav-item"> - <a class="nav-link" data-toggle="tab" href="#sieve-widget-help" data-i18n="fileinto.tab.help" role="tab"></a> + <a class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" data-i18n="fileinto.tab.help" role="tab"></a> </li> </ul> diff --git a/src/common/libSieve/extensions/RFC5228/templates/SieveHeaderTestUI.html b/src/common/libSieve/extensions/RFC5228/templates/SieveHeaderTestUI.html index 391f7f90..16460ad3 100644 --- a/src/common/libSieve/extensions/RFC5228/templates/SieveHeaderTestUI.html +++ b/src/common/libSieve/extensions/RFC5228/templates/SieveHeaderTestUI.html @@ -1,13 +1,13 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a class="nav-link active" data-i18n="header.tab.home" data-toggle="tab" href="#sieve-widget-headertest" role="tab"></a> + <a class="nav-link active" data-i18n="header.tab.home" data-bs-toggle="tab" href="#sieve-widget-headertest" role="tab"></a> </li> <li class="nav-item"> - <a class="nav-link" data-i18n="header.tab.advanced" data-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> + <a class="nav-link" data-i18n="header.tab.advanced" data-bs-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> </li> <li class="nav-item"> - <a class="nav-link" data-i18n="header.tab.help" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a class="nav-link" data-i18n="header.tab.help" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> @@ -22,7 +22,7 @@ </div> <div id="sivHeaderHeaderListTemplate" class="d-none"> - <div class="dropdown-menu dropdown-menu-right"> + <div class="dropdown-menu dropdown-menu-end"> <button class="dropdown-item" type="button">Subject</button> <button class="dropdown-item" type="button">Date</button> <button class="dropdown-item" type="button">Message-ID</button> diff --git a/src/common/libSieve/extensions/RFC5228/templates/SieveKeepActionUI.html b/src/common/libSieve/extensions/RFC5228/templates/SieveKeepActionUI.html index 154f68d4..d3e3972c 100644 --- a/src/common/libSieve/extensions/RFC5228/templates/SieveKeepActionUI.html +++ b/src/common/libSieve/extensions/RFC5228/templates/SieveKeepActionUI.html @@ -1,7 +1,7 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a class="nav-link active" data-toggle="tab" href="#sieve-widget-help" data-i18n="keep.tab.help" role="tab"></a> + <a class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-help" data-i18n="keep.tab.help" role="tab"></a> </li> </ul> diff --git a/src/common/libSieve/extensions/RFC5228/templates/SieveRedirectActionUI.html b/src/common/libSieve/extensions/RFC5228/templates/SieveRedirectActionUI.html index 2d36cfb9..350899fd 100644 --- a/src/common/libSieve/extensions/RFC5228/templates/SieveRedirectActionUI.html +++ b/src/common/libSieve/extensions/RFC5228/templates/SieveRedirectActionUI.html @@ -1,10 +1,10 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a class="nav-link active" data-i18n="redirect.tab.home" data-toggle="tab" href="#sieve-widget-redirect" role="tab"></a> + <a class="nav-link active" data-i18n="redirect.tab.home" data-bs-toggle="tab" href="#sieve-widget-redirect" role="tab"></a> </li> <li class="nav-item"> - <a class="nav-link" data-i18n="redirect.tab.help" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a class="nav-link" data-i18n="redirect.tab.help" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> diff --git a/src/common/libSieve/extensions/RFC5228/templates/SieveSizeTestUI.html b/src/common/libSieve/extensions/RFC5228/templates/SieveSizeTestUI.html index e96b9b83..de265c84 100644 --- a/src/common/libSieve/extensions/RFC5228/templates/SieveSizeTestUI.html +++ b/src/common/libSieve/extensions/RFC5228/templates/SieveSizeTestUI.html @@ -1,10 +1,10 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a class="nav-link active" data-i18n="size.tab.home" data-toggle="tab" href="#sieve-widget-size" role="tab"></a> + <a class="nav-link active" data-i18n="size.tab.home" data-bs-toggle="tab" href="#sieve-widget-size" role="tab"></a> </li> <li class="nav-item"> - <a class="nav-link" data-i18n="size.tab.help" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a class="nav-link" data-i18n="size.tab.help" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> diff --git a/src/common/libSieve/extensions/RFC5228/templates/SieveStopActionUI.html b/src/common/libSieve/extensions/RFC5228/templates/SieveStopActionUI.html index b15bd97e..db406803 100644 --- a/src/common/libSieve/extensions/RFC5228/templates/SieveStopActionUI.html +++ b/src/common/libSieve/extensions/RFC5228/templates/SieveStopActionUI.html @@ -1,7 +1,7 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a class="nav-link active" data-toggle="tab" href="#sieve-widget-help" data-i18n="stop.tab.help" role="tab"></a> + <a class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-help" data-i18n="stop.tab.help" role="tab"></a> </li> </ul> diff --git a/src/common/libSieve/extensions/RFC5228/widgets/SieveBlocksUI.mjs b/src/common/libSieve/extensions/RFC5228/widgets/SieveBlocksUI.mjs index 0237e718..75fa428d 100644 --- a/src/common/libSieve/extensions/RFC5228/widgets/SieveBlocksUI.mjs +++ b/src/common/libSieve/extensions/RFC5228/widgets/SieveBlocksUI.mjs @@ -31,7 +31,7 @@ class SieveRootNodeUI extends SieveAbstractBoxUI { * @inheritdoc */ createHtml(parent) { - parent.appendChild( + parent.append( this.getSieve().elms[FIRST_ELEMENT].html()); return parent; @@ -57,7 +57,7 @@ class SieveBlockUI extends SieveAbstractBoxUI { */ createBlockChild(item) { const child = document.createElement('div'); - child.appendChild(item); + child.append(item); child.classList.add("sivBlockChild"); return child; @@ -76,17 +76,17 @@ class SieveBlockUI extends SieveAbstractBoxUI { if (!item) continue; - elm.appendChild((new SieveDropBoxUI(this, "sivBlockSpacer")) + elm.append((new SieveDropBoxUI(this, "sivBlockSpacer")) .drop(new SieveBlockDropHandler(), sivElm) .html()); - elm.appendChild(this.createBlockChild(item)); + elm.append(this.createBlockChild(item)); } - elm.appendChild((new SieveDropBoxUI(this, "sivBlockSpacer")) + elm.append((new SieveDropBoxUI(this, "sivBlockSpacer")) .drop(new SieveBlockDropHandler()) .html()); - parent.appendChild(elm); + parent.append(elm); return parent; } } diff --git a/src/common/libSieve/extensions/RFC5228/widgets/SieveConditionsUI.mjs b/src/common/libSieve/extensions/RFC5228/widgets/SieveConditionsUI.mjs index f5766bc6..b93ba2f1 100644 --- a/src/common/libSieve/extensions/RFC5228/widgets/SieveConditionsUI.mjs +++ b/src/common/libSieve/extensions/RFC5228/widgets/SieveConditionsUI.mjs @@ -34,14 +34,14 @@ class SieveIfUI extends SieveBlockUI { createHtml(parent) { const test = document.createElement("div"); - test.appendChild(this.getSieve().test().html()); + test.append(this.getSieve().test().html()); test.classList.add("sivConditionalChild"); const elm = document.createElement("div"); elm.id = `sivElm${this.id()}`; elm.classList.add("sivConditional"); - elm.appendChild(test); - elm.appendChild(super.createHtml(parent)); + elm.append(test); + elm.append(super.createHtml(parent)); return elm; } @@ -60,7 +60,7 @@ class SieveElseUI extends SieveBlockUI { const elm = document.createElement("div"); elm.id = `sivElm${this.id()}`; elm.classList.add("sivConditional"); - elm.appendChild(super.createHtml(parent)); + elm.append(super.createHtml(parent)); return elm; } @@ -129,7 +129,7 @@ class SieveConditionUI extends SieveSourceBoxUI { const children = this.getSieve().children(); for (let i = 0; i < children.length; i++) { - elm2.appendChild((new SieveDropBoxUI(this, "sivConditionSpacer")) + elm2.append((new SieveDropBoxUI(this, "sivConditionSpacer")) .drop(new SieveConditionDropHandler(), children[i]) .html()); @@ -145,14 +145,14 @@ class SieveConditionUI extends SieveSourceBoxUI { condition.querySelector(".sivIconCode").addEventListener("click", (e) => { return this.onToggleView(e); }); - elm2.appendChild(condition); + elm2.append(condition); const child = item.querySelector(".sivConditionChild").cloneNode(true); - child.appendChild(children[i].html()); - elm2.appendChild(child); + child.append(children[i].html()); + elm2.append(child); } - elm2.appendChild((new SieveDropBoxUI(this, "sivConditionSpacer")) + elm2.append((new SieveDropBoxUI(this, "sivConditionSpacer")) .drop(new SieveConditionDropHandler()) .html()); @@ -160,7 +160,7 @@ class SieveConditionUI extends SieveSourceBoxUI { content.id = `${this.uniqueId}-summary`; while (elm2.children.length) - content.appendChild(elm2.firstChild); + content.append(elm2.firstChild); const code = item.querySelector(".sivConditionCode"); code.id = `${this.uniqueId}-code`; @@ -171,8 +171,8 @@ class SieveConditionUI extends SieveSourceBoxUI { return this.onToggleView(e); }); - parent.appendChild(content); - parent.appendChild(code); + parent.append(content); + parent.append(code); return parent; } diff --git a/src/common/libSieve/extensions/RFC5228/widgets/SieveOperatorsUI.mjs b/src/common/libSieve/extensions/RFC5228/widgets/SieveOperatorsUI.mjs index 5d66512c..d58bec8f 100644 --- a/src/common/libSieve/extensions/RFC5228/widgets/SieveOperatorsUI.mjs +++ b/src/common/libSieve/extensions/RFC5228/widgets/SieveOperatorsUI.mjs @@ -59,7 +59,7 @@ class SieveNotUI extends SieveSimpleBoxUI { const elm = (new SieveTemplate()).convert(FRAGMENT); elm .querySelector(".sivNotTest") - .appendChild(this.getSieve().test().html()); + .append(this.getSieve().test().html()); return elm; } @@ -158,22 +158,22 @@ class SieveAnyOfAllOfUI extends SieveDialogBoxUI { .drop(new SieveMultaryDropHandler(), test[TEST_ELEMENT]) .html(); - testElms.appendChild(dropbox); + testElms.append(dropbox); const ul = document.createElement("ul"); ul.classList.add("mb-0"); - ul.classList.add("pl-3"); + ul.classList.add("ps-3"); const li = document.createElement("li"); - li.appendChild(test[TEST_ELEMENT].html()); + li.append(test[TEST_ELEMENT].html()); li.classList.add("sivOperatorChild"); - ul.appendChild(li); + ul.append(li); - testElms.appendChild(ul); + testElms.append(ul); } - testElms.appendChild( + testElms.append( (new SieveDropBoxUI(this, "sivOperatorSpacer")) .drop(new SieveMultaryDropHandler()) .html()); diff --git a/src/common/libSieve/extensions/body/templates/SieveBodyTestUI.html b/src/common/libSieve/extensions/body/templates/SieveBodyTestUI.html index 304b4221..37dcbf94 100644 --- a/src/common/libSieve/extensions/body/templates/SieveBodyTestUI.html +++ b/src/common/libSieve/extensions/body/templates/SieveBodyTestUI.html @@ -1,17 +1,17 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="body.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-bodytest" + <a data-i18n="body.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-bodytest" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="body.tab.transform" class="nav-link" data-toggle="tab" href="#sieve-widget-transform" role="tab"></a> + <a data-i18n="body.tab.transform" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-transform" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="body.tab.advanced" class="nav-link" data-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> + <a data-i18n="body.tab.advanced" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="body.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="body.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> diff --git a/src/common/libSieve/extensions/convert/templates/SieveConvertUI.html b/src/common/libSieve/extensions/convert/templates/SieveConvertUI.html index e8ffeedc..ea1d9b9b 100644 --- a/src/common/libSieve/extensions/convert/templates/SieveConvertUI.html +++ b/src/common/libSieve/extensions/convert/templates/SieveConvertUI.html @@ -1,11 +1,11 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="convert.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-convert" + <a data-i18n="convert.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-convert" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="convert.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="convert.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> @@ -21,7 +21,7 @@ <div id="sivConvertTo" data-list-dropdown="#sivConvertTemplate"></div> <div id="sivConvertTemplate" class="d-none"> - <div class="dropdown-menu dropdown-menu-right"> + <div class="dropdown-menu dropdown-menu-end"> <button class="dropdown-item" type="button" data-value="image/tiff"> <div>image/tiff</div> </button> diff --git a/src/common/libSieve/extensions/date/templates/SieveCurrentDateTestUI.html b/src/common/libSieve/extensions/date/templates/SieveCurrentDateTestUI.html index 11076a58..38de2c30 100644 --- a/src/common/libSieve/extensions/date/templates/SieveCurrentDateTestUI.html +++ b/src/common/libSieve/extensions/date/templates/SieveCurrentDateTestUI.html @@ -1,16 +1,16 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="currentdate.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-date" role="tab"></a> + <a data-i18n="currentdate.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-date" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="currentdate.tab.timezone" class="nav-link" data-toggle="tab" href="#sieve-widget-timezone" role="tab"></a> + <a data-i18n="currentdate.tab.timezone" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-timezone" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="currentdate.tab.advanced" class="nav-link" data-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> + <a data-i18n="currentdate.tab.advanced" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="currentdate.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="currentdate.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> @@ -22,7 +22,7 @@ <div id="sivDateDatepart" data-list-dropdown="#sivDateDatepartTemplate"></div> <div id="sivDateDatepartTemplate" class="d-none"> - <div class="dropdown-menu dropdown-menu-right" style="min-width:100%"> + <div class="dropdown-menu dropdown-menu-end" style="min-width:100%"> <button class="dropdown-item" type="button" data-value="year"> <span data-i18n="datepart.year"></span> <small class="text-muted">- <span data-i18n="datepart.year.text"></span></small> diff --git a/src/common/libSieve/extensions/date/templates/SieveDateTestUI.html b/src/common/libSieve/extensions/date/templates/SieveDateTestUI.html index 20fe33f2..2c3183d0 100644 --- a/src/common/libSieve/extensions/date/templates/SieveDateTestUI.html +++ b/src/common/libSieve/extensions/date/templates/SieveDateTestUI.html @@ -1,16 +1,16 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="date.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-date" role="tab"></a> + <a data-i18n="date.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-date" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="date.tab.timezone" class="nav-link" data-toggle="tab" href="#sieve-widget-timezone" role="tab"></a> + <a data-i18n="date.tab.timezone" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-timezone" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="date.tab.advanced" class="nav-link" data-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> + <a data-i18n="date.tab.advanced" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="date.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="date.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> @@ -26,7 +26,7 @@ <div id="sivDateDatepart" data-list-dropdown="#sivDateDatepartTemplate"></div> <div id="sivDateDatepartTemplate" class="d-none"> - <div class="dropdown-menu dropdown-menu-right" style="min-width:100%"> + <div class="dropdown-menu dropdown-menu-end" style="min-width:100%"> <button class="dropdown-item" type="button" data-value="year"> <span data-i18n="datepart.year"></span> <small class="text-muted">- <span data-i18n="datepart.year.text"></span></small> diff --git a/src/common/libSieve/extensions/duplicate/templates/SieveDuplicateTestUI.html b/src/common/libSieve/extensions/duplicate/templates/SieveDuplicateTestUI.html index ae182fba..e0c86e28 100644 --- a/src/common/libSieve/extensions/duplicate/templates/SieveDuplicateTestUI.html +++ b/src/common/libSieve/extensions/duplicate/templates/SieveDuplicateTestUI.html @@ -1,19 +1,19 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="duplicate.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-duplicatetest" + <a data-i18n="duplicate.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-duplicatetest" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="duplicate.tab.tracking" class="nav-link" data-toggle="tab" href="#sieve-widget-tracking" + <a data-i18n="duplicate.tab.tracking" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-tracking" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="duplicate.tab.expiration" class="nav-link" data-toggle="tab" href="#sieve-widget-expiration" + <a data-i18n="duplicate.tab.expiration" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-expiration" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="duplicate.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="duplicate.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> diff --git a/src/common/libSieve/extensions/editheader/templates/SieveAddHeaderActionUI.html b/src/common/libSieve/extensions/editheader/templates/SieveAddHeaderActionUI.html index a6381663..fafdd1d1 100644 --- a/src/common/libSieve/extensions/editheader/templates/SieveAddHeaderActionUI.html +++ b/src/common/libSieve/extensions/editheader/templates/SieveAddHeaderActionUI.html @@ -1,11 +1,11 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="addheader.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-addheader" + <a data-i18n="addheader.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-addheader" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="addheader.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="addheader.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> @@ -27,12 +27,12 @@ </form> <h5 data-i18n="addheader.position"></h5> - <div class="form-check mr-sm-2"> + <div class="form-check me-sm-2"> <input type="radio" class="form-check-input" name="last" value="false" id="sieve-header-first" /> <label data-i18n="addheader.first" class="form-check-label" for="sieve-header-first"></label> </div> - <div class="form-check mr-sm-2"> + <div class="form-check me-sm-2"> <input type="radio" class="form-check-input" name="last" value="true" id="sieve-header-last" /> <label data-i18n="addheader.last" class="form-check-label" for="sieve-header-last"></label> </div> diff --git a/src/common/libSieve/extensions/editheader/templates/SieveDeleteHeaderActionUI.html b/src/common/libSieve/extensions/editheader/templates/SieveDeleteHeaderActionUI.html index 25527273..0e1f638e 100644 --- a/src/common/libSieve/extensions/editheader/templates/SieveDeleteHeaderActionUI.html +++ b/src/common/libSieve/extensions/editheader/templates/SieveDeleteHeaderActionUI.html @@ -1,15 +1,15 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="deleteheader.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-deleteheader" + <a data-i18n="deleteheader.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-deleteheader" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="deleteheader.tab.advanced" class="nav-link" data-toggle="tab" href="#sieve-widget-advanced" + <a data-i18n="deleteheader.tab.advanced" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="deleteheader.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="deleteheader.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> @@ -19,14 +19,14 @@ <div class="mb-3"> <h5 data-i18n="deleteheader.position"></h5> - <div class="form-check mr-sm-2"> + <div class="form-check me-sm-2"> <input type="radio" class="form-check-input" name="header-index" value="all" id="sieve-header-all" /> <label data-i18n="deleteheader.all" class="form-check-label" for="sieve-header-all"></label> </div> <div class="row align-items-center mt-2"> <div class="col-auto"> - <div class="form-check mr-sm-2"> + <div class="form-check me-sm-2"> <input type="radio" class="form-check-input" name="header-index" value="first" id="sieve-header-first" /> <label data-i18n="deleteheader.first" class="form-check-label" for="sieve-header-first"></label> @@ -40,7 +40,7 @@ <div class="row align-items-center mt-2"> <div class="col-auto"> - <div class="form-check mr-sm-2"> + <div class="form-check me-sm-2"> <input type="radio" class="form-check-input" name="header-index" value="last" id="sieve-header-last" /> <label data-i18n="deleteheader.last" class="form-check-label" for="sieve-header-last"></label> @@ -61,13 +61,13 @@ </form> <h5 data-i18n="deleteheader.value"></h5> - <div class="form-check mr-sm-2"> + <div class="form-check me-sm-2"> <input type="radio" class="form-check-input" name="header-value" value="any" id="sieve-header-value-any" /> <label data-i18n="deleteheader.any" class="form-check-label" for="sieve-header-value-any"></label> </div> - <div class="form-check mr-sm-2"> + <div class="form-check me-sm-2"> <input type="radio" class="form-check-input" name="header-value" value="some" id="sieve-header-value-some" /> <label data-i18n="deleteheader.some" class="form-check-label" for="sieve-header-value-some"></label> diff --git a/src/common/libSieve/extensions/environment/templates/SieveEnvironmentUI.html b/src/common/libSieve/extensions/environment/templates/SieveEnvironmentUI.html index 52594f70..690b4127 100644 --- a/src/common/libSieve/extensions/environment/templates/SieveEnvironmentUI.html +++ b/src/common/libSieve/extensions/environment/templates/SieveEnvironmentUI.html @@ -1,13 +1,13 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="environment.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-environment" role="tab"></a> + <a data-i18n="environment.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-environment" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="environment.tab.advanced" class="nav-link" data-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> + <a data-i18n="environment.tab.advanced" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="environment.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="environment.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> @@ -22,7 +22,7 @@ </div> <div id="sivEnvironmentNameTemplate" class="d-none"> - <div class="dropdown-menu dropdown-menu-right"> + <div class="dropdown-menu dropdown-menu-end"> <button class="dropdown-item" type="button" data-value="domain"> <div>domain</div> <small data-i18n="environment.name.domain" class="text-muted"></small> diff --git a/src/common/libSieve/extensions/imapflags/templates/SieveAddFlagActionUI.html b/src/common/libSieve/extensions/imapflags/templates/SieveAddFlagActionUI.html index f6237672..b9bcbb33 100644 --- a/src/common/libSieve/extensions/imapflags/templates/SieveAddFlagActionUI.html +++ b/src/common/libSieve/extensions/imapflags/templates/SieveAddFlagActionUI.html @@ -1,10 +1,10 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="addflag.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-addflag" role="tab"></a> + <a data-i18n="addflag.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-addflag" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="addflag.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="addflag.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> @@ -21,7 +21,7 @@ </div> <div id="sivFlagKeywordListTemplate" class="d-none"> - <div class="dropdown-menu dropdown-menu-right"> + <div class="dropdown-menu dropdown-menu-end"> <button class="dropdown-item" type="button">\Seen</button> <button class="dropdown-item" type="button">\Answered</button> <button class="dropdown-item" type="button">\Flagged</button> diff --git a/src/common/libSieve/extensions/imapflags/templates/SieveFlagsTag.html b/src/common/libSieve/extensions/imapflags/templates/SieveFlagsTag.html index 2ffaad84..e678e846 100644 --- a/src/common/libSieve/extensions/imapflags/templates/SieveFlagsTag.html +++ b/src/common/libSieve/extensions/imapflags/templates/SieveFlagsTag.html @@ -13,7 +13,7 @@ </div> <div id="sivFlagKeyListTemplate" class="d-none"> - <div class="dropdown-menu dropdown-menu-right"> + <div class="dropdown-menu dropdown-menu-end"> <button class="dropdown-item" type="button">\Seen</button> <button class="dropdown-item" type="button">\Answered</button> <button class="dropdown-item" type="button">\Flagged</button> diff --git a/src/common/libSieve/extensions/imapflags/templates/SieveHasFlagTestUI.html b/src/common/libSieve/extensions/imapflags/templates/SieveHasFlagTestUI.html index d6a45ddf..75dfacf5 100644 --- a/src/common/libSieve/extensions/imapflags/templates/SieveHasFlagTestUI.html +++ b/src/common/libSieve/extensions/imapflags/templates/SieveHasFlagTestUI.html @@ -1,15 +1,15 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="hasflag.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-hasflag" + <a data-i18n="hasflag.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-hasflag" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="hasflag.tab.advanced" class="nav-link" data-toggle="tab" href="#sieve-widget-advanced" + <a data-i18n="hasflag.tab.advanced" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="hasflag.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="hasflag.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> @@ -27,7 +27,7 @@ </div> <div id="sivHasFlagKeyListTemplate" class="d-none"> - <div class="dropdown-menu dropdown-menu-right"> + <div class="dropdown-menu dropdown-menu-end"> <button class="dropdown-item" type="button">\Seen</button> <button class="dropdown-item" type="button">\Answered</button> <button class="dropdown-item" type="button">\Flagged</button> diff --git a/src/common/libSieve/extensions/imapflags/templates/SieveRemoveFlagActionUI.html b/src/common/libSieve/extensions/imapflags/templates/SieveRemoveFlagActionUI.html index 416af90c..08b23ea4 100644 --- a/src/common/libSieve/extensions/imapflags/templates/SieveRemoveFlagActionUI.html +++ b/src/common/libSieve/extensions/imapflags/templates/SieveRemoveFlagActionUI.html @@ -1,10 +1,10 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="removeflag.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-removeflag" role="tab"></a> + <a data-i18n="removeflag.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-removeflag" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="removeflag.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="removeflag.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> @@ -21,7 +21,7 @@ </div> <div id="sivFlagKeywordListTemplate" class="d-none"> - <div class="dropdown-menu dropdown-menu-right"> + <div class="dropdown-menu dropdown-menu-end"> <button class="dropdown-item" type="button">\Seen</button> <button class="dropdown-item" type="button">\Answered</button> <button class="dropdown-item" type="button">\Flagged</button> diff --git a/src/common/libSieve/extensions/imapflags/templates/SieveSetFlagActionUI.html b/src/common/libSieve/extensions/imapflags/templates/SieveSetFlagActionUI.html index 1fcbaea8..f54ee64f 100644 --- a/src/common/libSieve/extensions/imapflags/templates/SieveSetFlagActionUI.html +++ b/src/common/libSieve/extensions/imapflags/templates/SieveSetFlagActionUI.html @@ -1,10 +1,10 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="setflags.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-setflag" role="tab"></a> + <a data-i18n="setflags.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-setflag" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="setflags.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="setflags.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> @@ -20,7 +20,7 @@ </div> <div id="sivFlagKeywordListTemplate" class="d-none"> - <div class="dropdown-menu dropdown-menu-right"> + <div class="dropdown-menu dropdown-menu-end"> <button class="dropdown-item" type="button">\Seen</button> <button class="dropdown-item" type="button">\Answered</button> <button class="dropdown-item" type="button">\Flagged</button> diff --git a/src/common/libSieve/extensions/include/template/SieveGlobalActionUI.html b/src/common/libSieve/extensions/include/template/SieveGlobalActionUI.html index 5a943a60..f2baf670 100644 --- a/src/common/libSieve/extensions/include/template/SieveGlobalActionUI.html +++ b/src/common/libSieve/extensions/include/template/SieveGlobalActionUI.html @@ -1,10 +1,10 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="global.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-global" role="tab"></a> + <a data-i18n="global.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-global" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="global.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="global.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> <div id="template-content"> diff --git a/src/common/libSieve/extensions/include/template/SieveIncludeActionUI.html b/src/common/libSieve/extensions/include/template/SieveIncludeActionUI.html index 70610d6c..0d219df4 100644 --- a/src/common/libSieve/extensions/include/template/SieveIncludeActionUI.html +++ b/src/common/libSieve/extensions/include/template/SieveIncludeActionUI.html @@ -1,13 +1,13 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="include.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-include" role="tab"></a> + <a data-i18n="include.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-include" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="include.tab.advanced" class="nav-link" data-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> + <a data-i18n="include.tab.advanced" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="include.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="include.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> diff --git a/src/common/libSieve/extensions/include/template/SieveReturnActionUI.html b/src/common/libSieve/extensions/include/template/SieveReturnActionUI.html index 2c76bab5..3e31f4d3 100644 --- a/src/common/libSieve/extensions/include/template/SieveReturnActionUI.html +++ b/src/common/libSieve/extensions/include/template/SieveReturnActionUI.html @@ -1,7 +1,7 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="return.tab.help" class="nav-link active" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="return.tab.help" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> <div id="template-content"> diff --git a/src/common/libSieve/extensions/mailbox/templates/SieveMailboxExistsTest.html b/src/common/libSieve/extensions/mailbox/templates/SieveMailboxExistsTest.html index dea74ec9..e43fcf34 100644 --- a/src/common/libSieve/extensions/mailbox/templates/SieveMailboxExistsTest.html +++ b/src/common/libSieve/extensions/mailbox/templates/SieveMailboxExistsTest.html @@ -1,10 +1,10 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="mailboxexists.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-mailboxexiststest" role="tab"></a> + <a data-i18n="mailboxexists.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-mailboxexiststest" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="mailboxexists.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="mailboxexists.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> diff --git a/src/common/libSieve/extensions/mailbox/templates/SieveMetaDataExistsTest.html b/src/common/libSieve/extensions/mailbox/templates/SieveMetaDataExistsTest.html index e81cb924..267a7180 100644 --- a/src/common/libSieve/extensions/mailbox/templates/SieveMetaDataExistsTest.html +++ b/src/common/libSieve/extensions/mailbox/templates/SieveMetaDataExistsTest.html @@ -1,10 +1,10 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="metadataexists.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-metadataexiststest" role="tab"></a> + <a data-i18n="metadataexists.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-metadataexiststest" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="metadataexists.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab">Help</a> + <a data-i18n="metadataexists.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab">Help</a> </li> </ul> diff --git a/src/common/libSieve/extensions/mailbox/templates/SieveMetaDataTest.html b/src/common/libSieve/extensions/mailbox/templates/SieveMetaDataTest.html index 90f70449..f90974c8 100644 --- a/src/common/libSieve/extensions/mailbox/templates/SieveMetaDataTest.html +++ b/src/common/libSieve/extensions/mailbox/templates/SieveMetaDataTest.html @@ -1,15 +1,15 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="metadata.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-stringtest" + <a data-i18n="metadata.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-stringtest" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="metadata.tab.advanced" class="nav-link" data-toggle="tab" href="#sieve-widget-advanced" + <a data-i18n="metadata.tab.advanced" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="metadata.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="metadata.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> diff --git a/src/common/libSieve/extensions/mailbox/templates/SieveServerMetaDataExistsTest.html b/src/common/libSieve/extensions/mailbox/templates/SieveServerMetaDataExistsTest.html index 0f626525..cb8faa01 100644 --- a/src/common/libSieve/extensions/mailbox/templates/SieveServerMetaDataExistsTest.html +++ b/src/common/libSieve/extensions/mailbox/templates/SieveServerMetaDataExistsTest.html @@ -1,10 +1,10 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="servermetadataexists.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-servermetadataexiststest" role="tab"></a> + <a data-i18n="servermetadataexists.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-servermetadataexiststest" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="servermetadataexists.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="servermetadataexists.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> diff --git a/src/common/libSieve/extensions/mailbox/templates/SieveServerMetaDataTest.html b/src/common/libSieve/extensions/mailbox/templates/SieveServerMetaDataTest.html index cb6af75f..a20837b5 100644 --- a/src/common/libSieve/extensions/mailbox/templates/SieveServerMetaDataTest.html +++ b/src/common/libSieve/extensions/mailbox/templates/SieveServerMetaDataTest.html @@ -1,13 +1,13 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="servermetadata.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-stringtest" role="tab"></a> + <a data-i18n="servermetadata.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-stringtest" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="servermetadata.tab.advanced" class="nav-link" data-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> + <a data-i18n="servermetadata.tab.advanced" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="servermetadata.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="servermetadata.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> diff --git a/src/common/libSieve/extensions/notify/templates/SieveNotifyActionUI.html b/src/common/libSieve/extensions/notify/templates/SieveNotifyActionUI.html index e787f591..6ae4ceed 100644 --- a/src/common/libSieve/extensions/notify/templates/SieveNotifyActionUI.html +++ b/src/common/libSieve/extensions/notify/templates/SieveNotifyActionUI.html @@ -1,13 +1,13 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="notify.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-notifyaction" role="tab"></a> + <a data-i18n="notify.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-notifyaction" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="notify.tab.advanced" class="nav-link" data-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> + <a data-i18n="notify.tab.advanced" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="notify.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="notify.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> @@ -19,7 +19,7 @@ </div> <div id="sivNotifyMethodTemplate" class="d-none"> - <div class="dropdown-menu dropdown-menu-right" style="min-width:100%"> + <div class="dropdown-menu dropdown-menu-end" style="min-width:100%"> <button class="dropdown-item" type="button" data-value="tel:+1234567890"> <div data-i18n="notify.method.tel"></div> <div data-i18n="notify.method.tel.text" class="text-muted"></div> diff --git a/src/common/libSieve/extensions/notify/templates/SieveNotifyMethodCapabilityTestUI.html b/src/common/libSieve/extensions/notify/templates/SieveNotifyMethodCapabilityTestUI.html index 59112ede..bd814c9f 100644 --- a/src/common/libSieve/extensions/notify/templates/SieveNotifyMethodCapabilityTestUI.html +++ b/src/common/libSieve/extensions/notify/templates/SieveNotifyMethodCapabilityTestUI.html @@ -1,13 +1,13 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="notifymethodcapability.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-notifymethodcapability" role="tab"></a> + <a data-i18n="notifymethodcapability.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-notifymethodcapability" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="notifymethodcapability.tab.advanced" class="nav-link" data-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> + <a data-i18n="notifymethodcapability.tab.advanced" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="notifymethodcapability.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="notifymethodcapability.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> @@ -20,7 +20,7 @@ </div> <div id="sivNotifyUriTemplate" class="d-none"> - <div class="dropdown-menu dropdown-menu-right" style="min-width:100%"> + <div class="dropdown-menu dropdown-menu-end" style="min-width:100%"> <button class="dropdown-item" type="button" data-value="tel:+1234567890"> <div data-i18n="notifymethodcapability.method.tel"></div> <div data-i18n="notifymethodcapability.method.tel.text" class="text-muted"></div> @@ -52,7 +52,7 @@ </div> <div id="sivNotifyKeyListTemplate" class="d-none"> - <div class="dropdown-menu dropdown-menu-right" style="min-width:100%"> + <div class="dropdown-menu dropdown-menu-start" style="min-width:100%"> <button class="dropdown-item" type="button" data-value="yes"> <span data-i18n="notifymethodcapability.yes"></span><br /> <small class="text-muted" data-i18n="notifymethodcapability.yes.text"> diff --git a/src/common/libSieve/extensions/notify/templates/SieveValidNotifyMethodTestUI.html b/src/common/libSieve/extensions/notify/templates/SieveValidNotifyMethodTestUI.html index 82ea91d7..cef039c0 100644 --- a/src/common/libSieve/extensions/notify/templates/SieveValidNotifyMethodTestUI.html +++ b/src/common/libSieve/extensions/notify/templates/SieveValidNotifyMethodTestUI.html @@ -1,10 +1,10 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="validnotifymethod.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-validnotifymethodtest" role="tab"></a> + <a data-i18n="validnotifymethod.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-validnotifymethodtest" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="validnotifymethod.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="validnotifymethod.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> diff --git a/src/common/libSieve/extensions/pipe/templates/SieveExecuteUI.html b/src/common/libSieve/extensions/pipe/templates/SieveExecuteUI.html index 28e604ad..15bbba3d 100644 --- a/src/common/libSieve/extensions/pipe/templates/SieveExecuteUI.html +++ b/src/common/libSieve/extensions/pipe/templates/SieveExecuteUI.html @@ -1,11 +1,11 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="execute.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-execute" + <a data-i18n="execute.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-execute" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="execute.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="execute.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> diff --git a/src/common/libSieve/extensions/pipe/templates/SieveFilterUI.html b/src/common/libSieve/extensions/pipe/templates/SieveFilterUI.html index 177cf760..78747701 100644 --- a/src/common/libSieve/extensions/pipe/templates/SieveFilterUI.html +++ b/src/common/libSieve/extensions/pipe/templates/SieveFilterUI.html @@ -1,11 +1,11 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="filter.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-filter" + <a data-i18n="filter.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-filter" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="filter.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="filter.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> diff --git a/src/common/libSieve/extensions/pipe/templates/SievePipeUI.html b/src/common/libSieve/extensions/pipe/templates/SievePipeUI.html index 46975c08..b6caa877 100644 --- a/src/common/libSieve/extensions/pipe/templates/SievePipeUI.html +++ b/src/common/libSieve/extensions/pipe/templates/SievePipeUI.html @@ -1,11 +1,11 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="pipe.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-pipe" + <a data-i18n="pipe.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-pipe" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="pipe.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="pipe.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> diff --git a/src/common/libSieve/extensions/reject/templates/SieveExtendedRejectActionUI.html b/src/common/libSieve/extensions/reject/templates/SieveExtendedRejectActionUI.html index 3c5c2d77..f905a348 100644 --- a/src/common/libSieve/extensions/reject/templates/SieveExtendedRejectActionUI.html +++ b/src/common/libSieve/extensions/reject/templates/SieveExtendedRejectActionUI.html @@ -1,11 +1,11 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="ereject.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-reject" + <a data-i18n="ereject.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-reject" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="ereject.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="ereject.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> diff --git a/src/common/libSieve/extensions/reject/templates/SieveRejectActionUI.html b/src/common/libSieve/extensions/reject/templates/SieveRejectActionUI.html index e81b3944..513ed4c1 100644 --- a/src/common/libSieve/extensions/reject/templates/SieveRejectActionUI.html +++ b/src/common/libSieve/extensions/reject/templates/SieveRejectActionUI.html @@ -1,11 +1,11 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="reject.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-reject" + <a data-i18n="reject.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-reject" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="reject.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="reject.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> diff --git a/src/common/libSieve/extensions/spamtest/templates/SieveSpamtestPlusValue.html b/src/common/libSieve/extensions/spamtest/templates/SieveSpamtestPlusValue.html index 85d9b954..58b1d05e 100644 --- a/src/common/libSieve/extensions/spamtest/templates/SieveSpamtestPlusValue.html +++ b/src/common/libSieve/extensions/spamtest/templates/SieveSpamtestPlusValue.html @@ -9,7 +9,7 @@ </div> <div id="sivSpamtestTemplate" class="d-none"> - <div class="dropdown-menu dropdown-menu-right"> + <div class="dropdown-menu dropdown-menu-end"> <button class="dropdown-item" type="button" data-value="0">0 <small data-i18n="spamtest.absolute.nottested" class="text-muted"></small> </button> @@ -43,7 +43,7 @@ </div> <div id="sivSpamtestPercentTemplate" class="d-none"> - <div class="dropdown-menu dropdown-menu-right"> + <div class="dropdown-menu dropdown-menu-start"> <button class="dropdown-item" type="button" data-value="0">0% <small data-i18n="spamtest.percent.nospam" class="text-muted"></small> </button> diff --git a/src/common/libSieve/extensions/spamtest/templates/SieveSpamtestUI.html b/src/common/libSieve/extensions/spamtest/templates/SieveSpamtestUI.html index cc31988a..4558e6fa 100644 --- a/src/common/libSieve/extensions/spamtest/templates/SieveSpamtestUI.html +++ b/src/common/libSieve/extensions/spamtest/templates/SieveSpamtestUI.html @@ -1,13 +1,13 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="spamtest.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-spamtest" role="tab"></a> + <a data-i18n="spamtest.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-spamtest" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="spamtest.tab.advanced" class="nav-link" data-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> + <a data-i18n="spamtest.tab.advanced" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="spamtest.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="spamtest.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> diff --git a/src/common/libSieve/extensions/spamtest/templates/SieveSpamtestValue.html b/src/common/libSieve/extensions/spamtest/templates/SieveSpamtestValue.html index c171bb25..d488d4eb 100644 --- a/src/common/libSieve/extensions/spamtest/templates/SieveSpamtestValue.html +++ b/src/common/libSieve/extensions/spamtest/templates/SieveSpamtestValue.html @@ -3,7 +3,7 @@ </div> <div id="sivSpamtestTemplate" class="d-none"> - <div class="dropdown-menu dropdown-menu-right"> + <div class="dropdown-menu dropdown-menu-end"> <button class="dropdown-item" type="button" data-value="0">0 <small data-i18n="spamtest.absolute.nottested" class="text-muted"></small> </button> diff --git a/src/common/libSieve/extensions/spamtest/templates/SieveVirustestUI.html b/src/common/libSieve/extensions/spamtest/templates/SieveVirustestUI.html index 40e3f588..b469698d 100644 --- a/src/common/libSieve/extensions/spamtest/templates/SieveVirustestUI.html +++ b/src/common/libSieve/extensions/spamtest/templates/SieveVirustestUI.html @@ -1,13 +1,13 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="virustest.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-virustest" role="tab"></a> + <a data-i18n="virustest.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-virustest" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="virustest.tab.advanced" class="nav-link" data-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> + <a data-i18n="virustest.tab.advanced" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="virustest.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="virustest.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> @@ -24,7 +24,7 @@ </div> <div id="sivVirustestTemplate" class="d-none"> - <div class="dropdown-menu dropdown-menu-right"> + <div class="dropdown-menu dropdown-menu-end"> <button class="dropdown-item" type="button" data-value="0">0 <small data-i18n="virustest.nottested" class="text-muted"></small> </button> diff --git a/src/common/libSieve/extensions/spamtest/widgets/SieveSpamtestUI.mjs b/src/common/libSieve/extensions/spamtest/widgets/SieveSpamtestUI.mjs index 1609367f..d7feb9b4 100644 --- a/src/common/libSieve/extensions/spamtest/widgets/SieveSpamtestUI.mjs +++ b/src/common/libSieve/extensions/spamtest/widgets/SieveSpamtestUI.mjs @@ -131,7 +131,7 @@ class SieveSpamtestUI extends SieveTestDialogBoxUI { const elm = await ((new SieveTemplate()) .load("./extensions/spamtest/templates/SieveSpamtestPlusValue.html")); - document.querySelector("#sivSpamtestPlaceholder").appendChild(elm); + document.querySelector("#sivSpamtestPlaceholder").append(elm); this.onLoadPercentualValue(); })(); @@ -142,7 +142,7 @@ class SieveSpamtestUI extends SieveTestDialogBoxUI { const elm = await ((new SieveTemplate()) .load("./extensions/spamtest/templates/SieveSpamtestValue.html")); - document.querySelector("#sivSpamtestPlaceholder").appendChild(elm); + document.querySelector("#sivSpamtestPlaceholder").append(elm); this.onLoadValue(); })(); diff --git a/src/common/libSieve/extensions/vacation-seconds/template/SieveVacationIntervalSecondsUI.html b/src/common/libSieve/extensions/vacation-seconds/template/SieveVacationIntervalSecondsUI.html index d39a7e1e..91a5bb6c 100644 --- a/src/common/libSieve/extensions/vacation-seconds/template/SieveVacationIntervalSecondsUI.html +++ b/src/common/libSieve/extensions/vacation-seconds/template/SieveVacationIntervalSecondsUI.html @@ -4,7 +4,7 @@ value=":seconds" /> <label class="form-label d-flex align-items-baseline flex-wrap"> <span style="white-space: pre" class="form-label" data-i18n="interval.seconds.label.pre"></span> - <input class="form-control mr-1 ml-1" style="width:7em" type="number" min="0" id="txtVacationIntervalSeconds" /> + <input class="form-control me-1 ms-1" style="width:7em" type="number" min="0" id="txtVacationIntervalSeconds" /> <span class="form-label" data-i18n="interval.seconds.label.post"></span> </label> </div> diff --git a/src/common/libSieve/extensions/vacation/template/SieveVacationIntervalDaysUI.html b/src/common/libSieve/extensions/vacation/template/SieveVacationIntervalDaysUI.html index a0d491e0..d9b3ad4f 100644 --- a/src/common/libSieve/extensions/vacation/template/SieveVacationIntervalDaysUI.html +++ b/src/common/libSieve/extensions/vacation/template/SieveVacationIntervalDaysUI.html @@ -3,7 +3,7 @@ <input class="form-check-input mt-2" type="radio" id="cbxVacationIntervalDays" name="interval" value=":days" /> <label class="form-label d-flex align-items-baseline flex-wrap"> <span style="white-space: pre" class="form-label" data-i18n="interval.days.label.pre"></span> - <input class="form-control mr-1 ml-1" style="width:7em" type="number" id="txtVacationIntervalDays" min="0" /> + <input class="form-control me-1 ms-1" style="width:7em" type="number" id="txtVacationIntervalDays" min="0" /> <span class="form-label" data-i18n="interval.days.label.post"></span> </label> </div> diff --git a/src/common/libSieve/extensions/vacation/template/SieveVacationUI.html b/src/common/libSieve/extensions/vacation/template/SieveVacationUI.html index 3a550c0b..b0214265 100644 --- a/src/common/libSieve/extensions/vacation/template/SieveVacationUI.html +++ b/src/common/libSieve/extensions/vacation/template/SieveVacationUI.html @@ -1,16 +1,16 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="vacation.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-vacation" role="tab"></a> + <a data-i18n="vacation.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-vacation" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="vacation.tab.envelope" class="nav-link" data-toggle="tab" href="#sieve-widget-envelope" role="tab"></a> + <a data-i18n="vacation.tab.envelope" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-envelope" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="vacation.tab.advanced" class="nav-link" data-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> + <a data-i18n="vacation.tab.advanced" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="vacation.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="vacation.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> @@ -22,21 +22,21 @@ <div id="vacationEnvelopeEdit"> <div class="d-flex"> <div data-i18n="vacation.envelope.subject"></div> - <div class="flex-grow-1 text-truncate text-secondary pl-1"> + <div class="flex-grow-1 text-truncate text-secondary ps-1"> <span id="vacationSubjectDesc"></span> <span class="d-none" id="vacationSubjectDescDefault" data-i18n="vacation.envelope.subject.default"></span> </div> </div> <div class="d-flex"> <div data-i18n="vacation.envelope.from"></div> - <div class="flex-grow-1 text-truncate text-secondary pl-1"> + <div class="flex-grow-1 text-truncate text-secondary ps-1"> <span id="vacationFromDesc"></span> <span class="d-none" id="vacationFromDescDefault" data-i18n="vacation.envelope.from.default"></span> </div> </div> <div class="d-flex"> <div data-i18n="vacation.envelope.addresses"></div> - <div id="vacationAddressesDesc" class="flex-grow-1 text-truncate text-secondary pl-1"></div> + <div id="vacationAddressesDesc" class="flex-grow-1 text-truncate text-secondary ps-1"></div> </div> </div> <br/> diff --git a/src/common/libSieve/extensions/vacation/widgets/SieveVacationUI.mjs b/src/common/libSieve/extensions/vacation/widgets/SieveVacationUI.mjs index 44c91ac2..bb46155f 100644 --- a/src/common/libSieve/extensions/vacation/widgets/SieveVacationUI.mjs +++ b/src/common/libSieve/extensions/vacation/widgets/SieveVacationUI.mjs @@ -179,14 +179,14 @@ class SieveVacationUI extends SieveActionDialogBoxUI { (new SieveOverlayWidget("action/vacation/interval/", "#sivVacationIntervalOverlay")) .init(this.getSieve()); - document.querySelector('a[data-toggle="tab"][href="#sieve-widget-envelope"]') + document.querySelector('a[data-bs-toggle="tab"][href="#sieve-widget-envelope"]') .addEventListener('hide.bs.tab', () => { this.onEnvelopeChanged(); }); document.querySelector("#vacationEnvelopeEdit").addEventListener("click", () => { document - .querySelector(`a[data-toggle="tab"][href="#sieve-widget-envelope"]`) + .querySelector(`a[data-bs-toggle="tab"][href="#sieve-widget-envelope"]`) .click(); }); diff --git a/src/common/libSieve/extensions/variables/templates/SieveSetActionUI.html b/src/common/libSieve/extensions/variables/templates/SieveSetActionUI.html index 91dc4b95..eab18c86 100644 --- a/src/common/libSieve/extensions/variables/templates/SieveSetActionUI.html +++ b/src/common/libSieve/extensions/variables/templates/SieveSetActionUI.html @@ -1,15 +1,15 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="set.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-setaction" + <a data-i18n="set.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-setaction" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="set.tab.advanced" class="nav-link" data-toggle="tab" href="#sieve-widget-advanced" + <a data-i18n="set.tab.advanced" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="set.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="set.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> diff --git a/src/common/libSieve/extensions/variables/templates/SieveStringTestUI.html b/src/common/libSieve/extensions/variables/templates/SieveStringTestUI.html index 36d5ca58..90565ed5 100644 --- a/src/common/libSieve/extensions/variables/templates/SieveStringTestUI.html +++ b/src/common/libSieve/extensions/variables/templates/SieveStringTestUI.html @@ -1,15 +1,15 @@ <div> <ul id="template-tabs"> <li class="nav-item"> - <a data-i18n="string.tab.home" class="nav-link active" data-toggle="tab" href="#sieve-widget-stringtest" + <a data-i18n="string.tab.home" class="nav-link active" data-bs-toggle="tab" href="#sieve-widget-stringtest" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="string.tab.advanced" class="nav-link" data-toggle="tab" href="#sieve-widget-advanced" + <a data-i18n="string.tab.advanced" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-advanced" role="tab"></a> </li> <li class="nav-item"> - <a data-i18n="string.tab.help" class="nav-link" data-toggle="tab" href="#sieve-widget-help" role="tab"></a> + <a data-i18n="string.tab.help" class="nav-link" data-bs-toggle="tab" href="#sieve-widget-help" role="tab"></a> </li> </ul> diff --git a/src/common/libSieve/extensions/variables/widgets/SieveVariablesUI.mjs b/src/common/libSieve/extensions/variables/widgets/SieveVariablesUI.mjs index 4916b006..f9f72772 100644 --- a/src/common/libSieve/extensions/variables/widgets/SieveVariablesUI.mjs +++ b/src/common/libSieve/extensions/variables/widgets/SieveVariablesUI.mjs @@ -96,7 +96,7 @@ class SieveSetActionUI extends SieveActionDialogBoxUI { // Sort the modifiers... let modifiers = document.querySelectorAll(`${widget.selector} .sieve-modifier`); - modifiers = Array.from(modifiers).sort((lhs, rhs) => { + modifiers = [...modifiers].sort((lhs, rhs) => { rhs = rhs.querySelector("input[type='checkbox'][name^='modifier/']").name; lhs = lhs.querySelector("input[type='checkbox'][name^='modifier/']").name; @@ -104,7 +104,7 @@ class SieveSetActionUI extends SieveActionDialogBoxUI { }); for (const modifier of modifiers) - document.querySelector(`${widget.selector}`).appendChild(modifier); + document.querySelector(`${widget.selector}`).append(modifier); } /** diff --git a/src/common/libSieve/templates/debug.html b/src/common/libSieve/templates/debug.html index 6a6e8148..c4985cd1 100644 --- a/src/common/libSieve/templates/debug.html +++ b/src/common/libSieve/templates/debug.html @@ -2,15 +2,15 @@ <div class="card-header d-flex justify-content-between py-0"> <ul class="nav nav-tabs card-header-tabs my-0 pt-3" role="tablist"> <li class="nav-item"> - <a data-i18n="debug.tab.script" class="sieve-accounts-tab nav-link active" data-toggle="tab" role="tab" + <a data-i18n="debug.tab.script" class="sieve-accounts-tab nav-link active" data-bs-toggle="tab" role="tab" href="#debugscript"></a> </li> <li class="nav-item"> - <a data-i18n="debug.tab.capabilities" class="sieve-settings-tab nav-link" data-toggle="tab" role="tab" + <a data-i18n="debug.tab.capabilities" class="sieve-settings-tab nav-link" data-bs-toggle="tab" role="tab" href="#debugcapabilities"></a> </li> <li class="nav-item"> - <a data-i18n="debug.tab.advanced" class="sieve-settings-tab nav-link" data-toggle="tab" role="tab" + <a data-i18n="debug.tab.advanced" class="sieve-settings-tab nav-link" data-bs-toggle="tab" role="tab" href="#debugadvanced"></a> </li> </ul> diff --git a/src/common/libSieve/templates/sidebar.html b/src/common/libSieve/templates/sidebar.html index b544d71a..b93113ea 100644 --- a/src/common/libSieve/templates/sidebar.html +++ b/src/common/libSieve/templates/sidebar.html @@ -2,7 +2,7 @@ <div class="accordion" id="accordionExample"> <div class="accordion-item"> <h2 class="accordion-header" id="headingOne"> - <button data-i18n="sidebar.actions" class="accordion-button bg-light" type="button" data-toggle="collapse" data-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne"> + <button data-i18n="sidebar.actions" class="accordion-button bg-light" type="button" data-bs-toggle="collapse" data-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne"> </button> </h2> <div id="collapseOne" class="accordion-collapse collapse show" aria-labelledby="headingOne" data-parent="#accordionExample"> @@ -12,7 +12,7 @@ </div> <div class="accordion-item"> <h2 class="accordion-header" id="headingTwo"> - <button data-i18n="sidebar.tests" class="accordion-button collapsed bg-light" type="button" data-toggle="collapse" data-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo"> + <button data-i18n="sidebar.tests" class="accordion-button collapsed bg-light" type="button" data-bs-toggle="collapse" data-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo"> </button> </h2> <div id="collapseTwo" class="accordion-collapse collapse" aria-labelledby="headingTwo" data-parent="#accordionExample"> @@ -22,7 +22,7 @@ </div> <div class="accordion-item"> <h2 class="accordion-header" id="headingThree"> - <button data-i18n="sidebar.operators" class="accordion-button collapsed bg-light" type="button" data-toggle="collapse" data-target="#collapseThree" aria-expanded="false" aria-controls="collapseThree"> + <button data-i18n="sidebar.operators" class="accordion-button collapsed bg-light" type="button" data-bs-toggle="collapse" data-target="#collapseThree" aria-expanded="false" aria-controls="collapseThree"> </button> </h2> <div id="collapseThree" class="accordion-collapse collapse" aria-labelledby="headingThree" data-parent="#accordionExample"> diff --git a/src/common/libSieve/toolkit/SieveParser.mjs b/src/common/libSieve/toolkit/SieveParser.mjs index 08106f80..f68b51f2 100644 --- a/src/common/libSieve/toolkit/SieveParser.mjs +++ b/src/common/libSieve/toolkit/SieveParser.mjs @@ -191,7 +191,7 @@ class SieveParser { return result; } - if (isNaN(parseInt(length, 10))) + if (Number.isNaN(Number.parseInt(length, 10))) throw new Error("Extract failed, length parameter is not a number"); result = this.bytes(length); @@ -242,7 +242,7 @@ class SieveParser { if (this._pos + offset > this._data.length) throw new Error("Parser out of bounds"); - return !isNaN(parseInt(this._data.charAt(this._pos + offset), 10)); + return !Number.isNaN(Number.parseInt(this._data.charAt(this._pos + offset), 10)); } /** diff --git a/src/common/libSieve/toolkit/SieveScriptDOM.mjs b/src/common/libSieve/toolkit/SieveScriptDOM.mjs index 25ba8b4b..70843323 100644 --- a/src/common/libSieve/toolkit/SieveScriptDOM.mjs +++ b/src/common/libSieve/toolkit/SieveScriptDOM.mjs @@ -292,7 +292,7 @@ class SieveDocument { // scan for null nodes.. for (item in this._nodes) - if (whitelist.indexOf(this._nodes[item]) === NO_ELEMENT) + if (!whitelist.includes(this._nodes[item])) if (this._nodes[item].parent() === null) items.push(item); @@ -305,7 +305,7 @@ class SieveDocument { const it = items.shift(); for (item in this._nodes) - if (whitelist.indexOf(this._nodes[item]) === NO_ELEMENT) + if (!whitelist.includes(this._nodes[item])) if (this._nodes[item].parent().id() === it) items.push(item); diff --git a/src/common/libSieve/toolkit/events/DataTransfer.mjs b/src/common/libSieve/toolkit/events/DataTransfer.mjs index e2cc11d1..a001cc3c 100644 --- a/src/common/libSieve/toolkit/events/DataTransfer.mjs +++ b/src/common/libSieve/toolkit/events/DataTransfer.mjs @@ -53,10 +53,10 @@ class SieveDataTransfer { isBugFree() { // Check if we are running in electron... - if (window.navigator.userAgent.toLowerCase().indexOf(" electron/")) + if (window.navigator.userAgent.toLowerCase().includes(" electron/")) return false; - if (window.navigator.userAgent.toLowerCase().indexOf(" chrome/")) + if (window.navigator.userAgent.toLowerCase().includes(" chrome/")) return false; return true; diff --git a/src/common/libSieve/toolkit/logic/GenericAtoms.mjs b/src/common/libSieve/toolkit/logic/GenericAtoms.mjs index b343125b..4c11c8b5 100644 --- a/src/common/libSieve/toolkit/logic/GenericAtoms.mjs +++ b/src/common/libSieve/toolkit/logic/GenericAtoms.mjs @@ -415,7 +415,7 @@ class SieveGenericDependentItem extends SieveGenericMandatoryItem { * This also means they have an implicit default value which makes parsing awkward. * * In case the tag is missing (which means using the implicit default) the class is fully transparent. - * Otherwise it is greedy and eats leading and tailing whitespaces. + * Otherwise it is greedy and eats leading and trailing whitespaces. */ class SieveGenericOptionalItem extends SieveAbstractGeneric { @@ -749,7 +749,7 @@ class SieveGenericStructure extends SieveAbstractElement { try { element.parse(parser); } - catch (ex) { + catch { // TODO reset item // Reset the position as if nothing happened parser.pos(pos); @@ -769,7 +769,7 @@ class SieveGenericStructure extends SieveAbstractElement { try { element.parse(parser); - } catch (ex) { + } catch { prev.enabled = false; diff --git a/src/common/libSieve/toolkit/logic/GenericElements.mjs b/src/common/libSieve/toolkit/logic/GenericElements.mjs index 78730f68..9e987be9 100644 --- a/src/common/libSieve/toolkit/logic/GenericElements.mjs +++ b/src/common/libSieve/toolkit/logic/GenericElements.mjs @@ -326,6 +326,7 @@ function initTests() { * * @param {*} capabilities */ +// eslint-disable-next-line no-unused-vars function createGrammar(capabilities) { initActions(); initTests(); diff --git a/src/common/libSieve/toolkit/style/layout.css b/src/common/libSieve/toolkit/style/layout.css index d359b938..6fa059b0 100755 --- a/src/common/libSieve/toolkit/style/layout.css +++ b/src/common/libSieve/toolkit/style/layout.css @@ -1,9 +1,9 @@ body { - margin:0px; + margin: 0; } #content { - padding: 0px 10px; + padding: 0 10px; margin-left: 180px; } @@ -14,20 +14,18 @@ body { } #toolbar .SivElement { - width:140px; + width: 140px; } -#sivTrash .sivTrashBin -{ +#sivTrash .sivTrashBin { height: 80px; - min-width:80px; - background-color:transparent; - background-image:url('trash.png'); - background-repeat:no-repeat; - background-position:center center; + min-width: 80px; + background-color: transparent; + background-image: url('trash.png'); + background-repeat: no-repeat; + background-position: center center; } -#sivTrash .sivTrashBin[data-sieve-dragging] -{ - background-image:url('trash-full.png'); +#sivTrash .sivTrashBin[data-sieve-dragging] { + background-image: url('trash-full.png'); } diff --git a/src/common/libSieve/toolkit/style/style.css b/src/common/libSieve/toolkit/style/style.css index c52fb3e5..6e1f46cc 100755 --- a/src/common/libSieve/toolkit/style/style.css +++ b/src/common/libSieve/toolkit/style/style.css @@ -1,17 +1,15 @@ .sivAction[data-sieve-flavour], .sivConditionText, -.sivConditionCode -{ - border: 1px #DDE4E9 solid; - background-color: #F3F6F7; - padding-left:3px; +.sivConditionCode { + border: 1px #dde4e9 solid; + background-color: #f3f6f7; + padding-left: 3px; cursor: default; } -.sivConditionCode code -{ +.sivConditionCode code { flex: auto 1 1; white-space: pre-wrap; } @@ -19,17 +17,17 @@ .sivConditionCode, .sivConditionText, .sivEditableElement[data-sieve-flavour], -.sivEditableElement -{ +.sivEditableElement { display: flex; flex-direction: row; } + .sivEditableElement > .sivSummaryContent, .sivEditableElement > .sivSummaryCode { flex: 1 1 auto; } -.sivEditableElement > .sivSummaryCode > code{ +.sivEditableElement > .sivSummaryCode > code { white-space: pre-wrap; } @@ -41,10 +39,8 @@ margin-bottom: inherit; } - -.sivEditableElement:hover -{ - border: 1px red #EDEDED solid; +.sivEditableElement:hover { + border: 1px red #ededed solid; background-color: white; box-shadow: 0 0 7px gray; } @@ -55,102 +51,91 @@ div[data-sieve-flavour][data-sieve-dragging] { box-shadow: 0 0 5px red; } -.sivSummaryContent em{ - font-family:courier; - font-style:normal; +.sivSummaryContent em { + font-family: courier, serif; + font-style: normal; } - - .sivEditableElement h1 { - font-size:1em; - padding:0px; - margin:0px; - } - +.sivEditableElement h1 { + font-size: 1em; + padding: 0; + margin: 0; +} /* Blocks */ -.sivBlock > .sivBlockChild:nth-last-of-type(2) -{ +.sivBlockChild { + background-image: url('vline.png'), url('hline.png'); + background-position: -7px center, 2px top; + background-repeat: repeat-y, no-repeat; +} + +.sivBlock > .sivBlockChild:nth-last-of-type(2) { background-image: url('hline.png'); background-position: 2px top; background-repeat: no-repeat; } +/* Dropmarkers */ +.sivBlockSpacer { + background-image: url('vline.png'); + background-position: -7px top; + background-repeat: repeat-y; -.sivBlock > .sivBlockSpacer:nth-last-of-type(1) -{ - background-image: none; - padding-bottom: 0px; + padding: 2px 0 2px 0; } -.sivBlockChild -{ - background-image: url('vline.png'), url('hline.png'); - background-position: -7px center, 2px top; - background-repeat: repeat-y, no-repeat; +.sivBlock > .sivBlockSpacer:nth-last-of-type(1) { + background-image: none; + padding-bottom: 0; } .sivConditionChild, .sivConditionSpacer:not(:first-of-type) { background-image: url('vline2.png'); - background-position: -4px center; + background-position: -4px center; background-repeat: repeat-y; } .sivBlockChild, .sivConditionChild { - padding-left:15px; + padding-left: 15px; } .sivConditionalChild { - padding:3px 0px 3px 3px; - background-color: #E8EDF0; - border: 1px #DDE4E9 solid -} - -/* Dropmarkers*/ -.sivBlockSpacer -{ - background-image: url('vline.png'); - background-position: -7px top; - background-repeat: repeat-y; - - padding:2px 0px 2px 0px; + padding: 3px 0 3px 3px; + background-color: #e8edf0; + border: 1px #dde4e9 solid; } -.sivConditionSpacer{ - padding:0px; +.sivConditionSpacer { + padding: 0; } .sivBlockSpacer, .sivOperatorSpacer, -.sivConditionSpacer -{ +.sivConditionSpacer { height: 6px; } .sivBlockSpacer > div, .sivConditionSpacer > div, -.sivOperatorSpacer > div -{ +.sivOperatorSpacer > div { height: 3px; } .sivBlockSpacer[data-sieve-dragging] > div, .sivConditionSpacer[data-sieve-dragging] > div, -.sivOperatorSpacer[data-sieve-dragging] > div -{ - background-color : red; - background-image: linear-gradient(rgba(255,255,255,.25), rgba(0,0,0,.15)); +.sivOperatorSpacer[data-sieve-dragging] > div { + background-color: red; + background-image: linear-gradient(rgba(255, 255, 255, 0.25), rgba(0, 0, 0, 0.15)); box-shadow: 0 0 5px red; } -#toolbar div[data-sieve-flavour] -{ - margin:2px 5px; - padding:0px 3px; +#toolbar div[data-sieve-flavour] { + margin: 2px 5px; + padding: 0 3px; background-color: lightgray; - border-radius:3px; + border-radius: 3px; border: 1px solid gray; } diff --git a/src/common/libSieve/toolkit/templates/SieveDropDownWidget.html b/src/common/libSieve/toolkit/templates/SieveDropDownWidget.html index 1febaaa8..565a5e69 100644 --- a/src/common/libSieve/toolkit/templates/SieveDropDownWidget.html +++ b/src/common/libSieve/toolkit/templates/SieveDropDownWidget.html @@ -4,9 +4,9 @@ <div class="sivDropDownWidget-active"> </div> </div> - <button class="btn btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" + <button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> </button> - <div class="dropdown-menu dropdown-menu-right sivDropDownWidget-menu" style="min-width:100%"> + <div class="dropdown-menu dropdown-menu-end sivDropDownWidget-menu" style="min-width:100%"> </div> </div> </div>
\ No newline at end of file diff --git a/src/common/libSieve/toolkit/templates/SieveNumericWidget.html b/src/common/libSieve/toolkit/templates/SieveNumericWidget.html index a2de597a..c8a720c6 100644 --- a/src/common/libSieve/toolkit/templates/SieveNumericWidget.html +++ b/src/common/libSieve/toolkit/templates/SieveNumericWidget.html @@ -1,8 +1,8 @@ <div> <div class="input-group col-md-8"> - <input class="form-control sieve-numeric-value text-right" aria-label="Text input with dropdown button" + <input class="form-control sieve-numeric-value text-end" aria-label="Text input with dropdown button" type="number" min="0" step="1"> </input> - <button class="btn btn-outline-secondary dropdown-toggle sieve-numeric-unit" type="button" data-toggle="dropdown" + <button class="btn btn-outline-secondary dropdown-toggle sieve-numeric-unit" type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Dropdown</button> <div class="dropdown-menu"> <button class="dropdown-item" type="button" data-value=""> diff --git a/src/common/libSieve/toolkit/templates/SieveStringListWidget.html b/src/common/libSieve/toolkit/templates/SieveStringListWidget.html index b98e5733..332a11e9 100644 --- a/src/common/libSieve/toolkit/templates/SieveStringListWidget.html +++ b/src/common/libSieve/toolkit/templates/SieveStringListWidget.html @@ -2,7 +2,7 @@ <div class="input-group input-group mb-1 string-list-item-template"> <input type="text" class="form-control col-lg-8" /> <button type="button" class="sieve-stringlist-dropdown d-none btn btn-outline-secondary dropdown-toggle-split" - data-toggle="dropdown"> + data-bs-toggle="dropdown"> <span class="sivIconMore"></span> </button> <button type="button" class="sieve-stringlist-delete btn btn-outline-secondary"> diff --git a/src/common/libSieve/toolkit/templates/SieveStringWidget.html b/src/common/libSieve/toolkit/templates/SieveStringWidget.html index a1c6162a..486d2c37 100644 --- a/src/common/libSieve/toolkit/templates/SieveStringWidget.html +++ b/src/common/libSieve/toolkit/templates/SieveStringWidget.html @@ -3,7 +3,7 @@ <input type="text" class="form-control col-lg-8 sieve-string-item" /> <button type="button" class="sieve-string-dropdown d-none btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" - data-toggle="dropdown"> + data-bs-toggle="dropdown"> <span class="sr-only">Toggle Dropdown</span> </button> </div> diff --git a/src/common/libSieve/toolkit/utils/SieveI18n.mjs b/src/common/libSieve/toolkit/utils/SieveI18n.mjs index 39742117..97e11950 100644 --- a/src/common/libSieve/toolkit/utils/SieveI18n.mjs +++ b/src/common/libSieve/toolkit/utils/SieveI18n.mjs @@ -132,7 +132,7 @@ class SieveI18n { try { await this.loadDictionary(`${path}${locale}.json`); - } catch (ex) { + } catch { // In case loading the dictionary failed e.g. due to a parsing error // we try falling back to our default one which is used during development. await this.loadDictionary(`${path}${DEFAULT_LOCALE}.json`); @@ -158,7 +158,7 @@ class SieveI18n { try { data = await (await fetch(dictionary, { cache: "no-store" })).text(); } catch (ex) { - this.getLogger().logI18n(`Failed to load dictionary ${dictionary}`); + this.getLogger().logI18n(`Loading dictionary ${dictionary} failed with error ${ex}`); throw new Error(`Failed to load dictionary ${dictionary}`); } diff --git a/src/common/libSieve/toolkit/widgets/Boxes.mjs b/src/common/libSieve/toolkit/widgets/Boxes.mjs index 46df0954..bb6f2eae 100644 --- a/src/common/libSieve/toolkit/widgets/Boxes.mjs +++ b/src/common/libSieve/toolkit/widgets/Boxes.mjs @@ -253,7 +253,7 @@ class SieveDropBoxUI extends SieveAbstractBoxUI { createHtml(parent) { parent.classList.add("sivDropBox"); parent.classList.add(this.name); - parent.appendChild(document.createElement("div")); + parent.append(document.createElement("div")); return parent; } @@ -335,7 +335,7 @@ class SieveSourceBoxUI extends SieveAbstractBoxUI { // update the code section const code = document.querySelector(`#${this.uniqueId}-code > code`); while (code.firstChild) - code.removeChild(code.firstChild); + code.firstChild.remove(); code.textContent = this.getSieve().toScript(); @@ -363,9 +363,9 @@ class SieveSimpleBoxUI extends SieveAbstractBoxUI { const summary = document.createElement("div"); summary.classList.add("sivSummaryContent"); summary.id = `${this.uniqueId}-summary`; - summary.appendChild(this.getSummary()); + summary.append(this.getSummary()); - parent.appendChild(summary); + parent.append(summary); return parent; } @@ -391,7 +391,7 @@ class SieveDialogBoxUI extends SieveSourceBoxUI { const body = document.querySelector("#sivDialogBody"); while (body.firstChild) - body.removeChild(body.firstChild); + body.firstChild.remove(); // Hide the dialog... bootstrap.Modal @@ -401,14 +401,14 @@ class SieveDialogBoxUI extends SieveSourceBoxUI { // update the summary section const summary = document.querySelector(`#${this.uniqueId}-summary`); while (summary.firstChild) - summary.removeChild(summary.firstChild); + summary.firstChild.remove(); - summary.appendChild(this.getSummary()); + summary.append(this.getSummary()); // update the code section const code = document.querySelector(`#${this.uniqueId}-code > code`); while (code.firstChild) - code.removeChild(code.firstChild); + code.firstChild.remove(); code.textContent = this.getSieve().toScript(); } @@ -419,7 +419,7 @@ class SieveDialogBoxUI extends SieveSourceBoxUI { async showEditor() { // TODO hide the save button in case we have only a help tab... - (new bootstrap.Modal(document.querySelector('#sivDialog2'))).show(); + (new bootstrap.Modal('#sivDialog2')).show(); const save = () => { this.save(); }; @@ -436,11 +436,11 @@ class SieveDialogBoxUI extends SieveSourceBoxUI { // Empty the existing dialog. const dialogTabs = document.querySelector("#sivDialogTabs"); while (dialogTabs.firstChild) - dialogTabs.removeChild(dialogTabs.firstChild); + dialogTabs.firstChild.remove(); const dialogBody = document.querySelector("#sivDialogBody"); while (dialogBody.firstChild) - dialogBody.removeChild(dialogBody.firstChild); + dialogBody.firstChild.remove(); const template = await (new SieveTemplate()).load(this.getTemplate()); @@ -450,7 +450,7 @@ class SieveDialogBoxUI extends SieveSourceBoxUI { throw new Error("Failed to load template, no tab section specified"); while (tabs.children.length) - dialogTabs.appendChild(tabs.firstChild); + dialogTabs.append(tabs.firstChild); const content = template.querySelector("#template-content"); @@ -458,7 +458,7 @@ class SieveDialogBoxUI extends SieveSourceBoxUI { throw new Error("Failed to load template, no content section specified"); while (content.children.length) - dialogBody.appendChild(content.firstChild); + dialogBody.append(content.firstChild); this.onLoad(); } @@ -508,18 +508,18 @@ class SieveDialogBoxUI extends SieveSourceBoxUI { return true; }); - content.appendChild(this.getSummary()); + content.append(this.getSummary()); // We need this container to make customizing the box easier. // e.g. for the allof/anyof operator. const div = document.createElement("div"); div.classList.add("sivEditableElement"); - div.appendChild(content); - div.appendChild(code); - div.appendChild(controls); + div.append(content); + div.append(code); + div.append(controls); - parent.appendChild(div); + parent.append(div); return parent; } diff --git a/src/common/libSieve/toolkit/widgets/Widgets.mjs b/src/common/libSieve/toolkit/widgets/Widgets.mjs index d2586d93..83c4ecc1 100644 --- a/src/common/libSieve/toolkit/widgets/Widgets.mjs +++ b/src/common/libSieve/toolkit/widgets/Widgets.mjs @@ -96,7 +96,7 @@ class SieveStringListWidget { if (this._min >= this.items().length) return; - item.parentNode.removeChild(item); + item.remove(); }); // connect the drop down menu... @@ -171,13 +171,13 @@ class SieveStringListWidget { const container = document.createElement("div"); const item = document.querySelector(`${this._selector} .sieve-stringlist-items`); - item.appendChild(container); + item.append(container); const template = (await (new SieveTemplate()) .load("./toolkit/templates/SieveStringListWidget.html")) .querySelector(".string-list-item-template"); - container.appendChild(template); + container.append(template); this.onItemAdded(container, value); return this; @@ -196,7 +196,7 @@ class SieveStringListWidget { const elm = document.querySelector(this._selector); while (elm.firstChild) - elm.removeChild(elm.firstChild); + elm.firstChild.remove(); const items = document.createElement("div"); items.classList.add("sieve-stringlist-items"); @@ -204,8 +204,8 @@ class SieveStringListWidget { const controls = document.createElement("div"); controls.classList.add("sieve-stringlist-control"); - elm.appendChild(items); - elm.appendChild(controls); + elm.append(items); + elm.append(controls); (async () => { @@ -213,14 +213,14 @@ class SieveStringListWidget { .load("./toolkit/templates/SieveStringListWidget.html")) .querySelector(".sieve-stringlist-add"); - controls.appendChild(template); + controls.append(template); controls .addEventListener("click", () => { this.addItem(); }); })(); - this._min = parseInt(elm.dataset.listMin, 10); + this._min = Number.parseInt(elm.dataset.listMin, 10); - if (isNaN(this._min)) + if (Number.isNaN(this._min)) this._min = DEFAULT_STRING_LIST_MIN; // init values if possible @@ -334,10 +334,10 @@ class SieveDropDownWidget { const elm = document.querySelector(this.selector); while (elm.firstChild) - elm.removeChild(elm.firstChild); + elm.firstChild.remove(); while (template.children.length) - elm.appendChild(template.firstChild); + elm.append(template.firstChild); this.initWidgets(sivElement); } @@ -448,11 +448,11 @@ class SieveAbstractItemWidget { const container = document.createElement("div"); while (template.children.length) - container.appendChild(template.firstChild); + container.append(template.firstChild); container.dataset.nodename = this.constructor.nodeName(); - this.getElement().appendChild(container); + this.getElement().append(container); this.load(sivElement); @@ -518,10 +518,10 @@ class SieveDropDownItemWidget extends SieveAbstractItemWidget { const activeElement = this.getActiveItem(); while (activeElement.firstChild) - activeElement.removeChild(activeElement.firstChild); + activeElement.firstChild.remove(); while (menuElement.firstChild) - activeElement.appendChild(menuElement.firstChild); + activeElement.append(menuElement.firstChild); activeElement.dataset.nodename = this.constructor.nodeName(); activeElement.dataset.value = menuElement.dataset.value; @@ -828,7 +828,7 @@ class SieveStringWidget { .load("./toolkit/templates/SieveStringWidget.html")) .querySelector(".string-item-template"); - document.querySelector(this._selector).appendChild(template); + document.querySelector(this._selector).append(template); this.setValue(value); } @@ -850,7 +850,7 @@ class SieveStringWidget { .querySelector(".sieve-string-dropdown"); button.classList.remove("d-none"); - button.insertAdjacentElement('afterend', menu); + button.after(menu); this.initClickHandler(menu); this.initUpdatables(menu); @@ -971,10 +971,10 @@ class SieveNumericWidget { const elm = document.querySelector(this._selector); while (elm.firstChild) - elm.removeChild(elm.firstChild); + elm.firstChild.remove(); while (template.children.length) - elm.appendChild(template.firstChild); + elm.append(template.firstChild); document .querySelector(`${this._selector} .sieve-numeric-value`) diff --git a/src/common/managesieve.ui/accounts/SieveAbstractAccountUI.mjs b/src/common/managesieve.ui/accounts/SieveAbstractAccountUI.mjs new file mode 100644 index 00000000..b6485744 --- /dev/null +++ b/src/common/managesieve.ui/accounts/SieveAbstractAccountUI.mjs @@ -0,0 +1,333 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +/* global bootstrap */ + +import { SieveIpcClient } from "./../utils/SieveIpcClient.mjs"; +import { SieveLogger } from "./../utils/SieveLogger.mjs"; +import { SieveTemplate } from "./../utils/SieveTemplate.mjs"; + +import { SieveScriptUI } from "./SieveScriptUI.mjs"; +import { SieveDebugSettingsUI } from "./../settings/ui/SieveDebugSettingsUI.mjs"; +import { SieveCapabilities } from "./SieveCapabilities.mjs"; + +const IS_SMALLER = -1; +const IS_EQUAL = 0; +const IS_LARGER = 1; + +/** + * A UI renderer for a sieve account + */ +class SieveAbstractAccountUI { + + /** + * Creates a new renderer for a sieve account. + * + * @param {SieveAccountsUI} accounts + * the parent sieve accounts renderer + * @param {string} id + * the account unique id. + */ + constructor(accounts, id) { + this.accounts = accounts; + this.id = id; + } + + /** + * Gets an instance to the logger. + * + * @returns {SieveLogger} + * an reference to the logger instance. + **/ + getLogger() { + return SieveLogger.getInstance(); + } + + /** + * Executes an action on the communication process. + * + * @param {string} action + * the actions unique name + * @param {object} [payload] + * the payload which should be send + * + * @returns {Promise<object>} + * the result received for this action. + */ + async send(action, payload) { + + if (typeof (payload) === "undefined" || payload === null) + payload = {}; + + if (typeof (payload) !== "object") + payload = { "data": payload }; + + payload["account"] = this.id; + + return await SieveIpcClient.sendMessage("core", action, payload); + } + + /** + * Checks if the current account has an active connection to the server + * + * @returns {boolean} true in case tha account is connected otherwise false + */ + async isConnected() { + return await this.send("account-connected"); + } + + /** + * Establishes a connection to the server + */ + async connect() { + + const item = await (new SieveTemplate()).load(`./accounts/account.connecting.html`); + + const scripts = document.querySelector(`#siv-account-${this.id} .siv-tpl-scripts`); + while (scripts.firstChild) + scripts.firstChild.remove(); + + scripts.append(item); + + await this.send("account-connect"); + await this.render(); + } + + /** + * Disconnects the account from the server. + */ + async disconnect() { + const item = await (new SieveTemplate()).load(`./accounts/account.disconnecting.html`); + + const scripts = document.querySelector(`#siv-account-${this.id} .siv-tpl-scripts`); + while (scripts.firstChild) + scripts.firstChild.remove(); + + scripts.append(item); + + await this.send("account-disconnect"); + await this.render(); + } + + /** + * Renders the settings pane + * + */ + async renderSettings() { + + const elm = await (new SieveTemplate()).load("./accounts/account.settings.html"); + + + const account = await this.send("account-get-settings"); + + + elm.querySelector(".sieve-settings-hostname") + .textContent = account.hostname; + elm.querySelector(".sieve-settings-port") + .textContent = account.port; + + if (!account.secure) + elm.querySelector(".sieve-settings-secure").style.display = 'none'; + + elm.querySelector(".sieve-settings-username") + .textContent = account.username; + + if (elm.querySelector(".sieve-settings-mechanism")) { + elm.querySelector(".sieve-settings-mechanism") + .textContent = account.mechanism; + } + + if (elm.querySelector(".sieve-settings-fingerprint")) { + elm.querySelector(".sieve-settings-fingerprint") + .textContent = account.fingerprint; + + if (account.fingerprint !== "") + elm.querySelector(".sieve-settings-fingerprint-item").classList.remove("d-none"); + } + + // Clear any existing left overs... + const settings = document.querySelector(`#siv-account-${this.id} .sieve-settings-content`); + while (settings.firstChild) + settings.firstChild.remove(); + + // ... and append the new element + settings.append(elm); + + if (elm.querySelector(".sieve-account-edit-debug")) { + elm.querySelector(".sieve-account-edit-debug") + .addEventListener("click", () => { this.showAdvancedSettings(); }); + } + } + + /** + * Renders the accounts outer ui + * + */ + async renderAccount() { + const elm = await (new SieveTemplate()).load("./accounts/account.html"); + + elm.id = `siv-account-${this.id}`; + + elm.querySelector(".sieve-accounts-content").id = `sieve-accounts-content-${this.id}`; + elm.querySelector(".sieve-accounts-tab").href = `#sieve-accounts-content-${this.id}`; + + elm.querySelector(".sieve-settings-content").id = `sieve-settings-content-${this.id}`; + elm.querySelector(".sieve-settings-tab").href = `#sieve-settings-content-${this.id}`; + elm.querySelector(".sieve-settings-tab").addEventListener('shown.bs.tab', () => { this.renderSettings(); }); + + elm.querySelector(".siv-account-name").textContent + = await this.send("account-get-displayname"); + + document.querySelector(".siv-accounts-items").append(elm); + + elm + .querySelector(".siv-account-create") + .addEventListener("click", () => { this.createScript(); }); + elm + .querySelector(".sieve-account-edit-settings") + .addEventListener("click", () => { this.showSettings(); }); + elm + .querySelector(".sieve-account-capabilities") + .addEventListener("click", () => { this.showCapabilities(); }); + elm + .querySelector(".sieve-account-reconnect-server") + .addEventListener("click", () => { this.connect(); }); + elm + .querySelector(".sieve-account-disconnect-server") + .addEventListener("click", () => { this.disconnect(); }); + } + + /** + * Renders the account ui's script pane + */ + async onRenderConnected() { + const data = await this.send("account-list"); + + const scripts = document.querySelector(`#siv-account-${this.id} .siv-tpl-scripts`); + + while (scripts.firstChild) + scripts.firstChild.remove(); + + if (!data.length) { + const elm = await (new SieveTemplate()).load(`./accounts/account.empty.html`); + scripts.append(elm); + + elm + .querySelector(".sieve-script-empty-create") + .addEventListener("click", () => { this.createScript(); }); + } + + // Sort the script names by their name... + data.sort((a, b) => { + const scriptA = a.script.toUpperCase(); + const scriptB = b.script.toUpperCase(); + + if (scriptA < scriptB) + return IS_SMALLER; + + if (scriptA > scriptB) + return IS_LARGER; + + return IS_EQUAL; + }); + + data.forEach(async (item) => { + this.getLogger().logWidget(`Rendering ${this.id}/${item.script}`); + await ((new SieveScriptUI(this, item.script, item.active)).render()); + }); + + } + + /** + * Called when the account should be rendered because of a disconnect. + */ + async onRenderDisconnected() { + + const elm = await (new SieveTemplate()).load(`./accounts/account.disconnected.html`); + + const scripts = document.querySelector(`#siv-account-${this.id} .siv-tpl-scripts`); + + while (scripts.firstChild) + scripts.firstChild.remove(); + + scripts.append(elm); + + elm.querySelector(".sieve-script-connect") + .addEventListener("click", () => { this.connect(); }); + } + + /** + * Renders the UI for this component. + */ + async render() { + this.getLogger().logWidget(`Rendering Account ${this.id}`); + + if (!document.querySelector(`#siv-account-${this.id}`)) { + await this.renderAccount(); + this.renderSettings(); + } + + if (await this.isConnected()) { + await this.onRenderConnected(); + return; + } + + await this.onRenderDisconnected("disconnected"); + } + + /** + * Shows the settings dialog + */ + showSettings() { + (new bootstrap.Tab(`#siv-account-${this.id} .sieve-settings-tab`)).show(); + } + + /** + * Show the advanced settings dialog + */ + showAdvancedSettings() { + (new SieveDebugSettingsUI(this)).show(); + } + + /** + * Shows the account's capabilities + * + * @returns {SieveAccountUI} + * a self reference. + */ + async showCapabilities() { + + if (await this.isConnected() === false) + return this; + + const capabilities = await this.send("account-capabilities"); + await (new SieveCapabilities()).show(capabilities); + + return this; + } + + /** + * Prompts for the new script name an creates the script + */ + async createScript() { + + if (await this.isConnected() === false) + return; + + const name = await this.send("script-create"); + + if (name !== "") + await this.render(); + } + +} + +export { SieveAbstractAccountUI }; diff --git a/src/common/managesieve.ui/accounts/SieveAbstractAccounts.js b/src/common/managesieve.ui/accounts/SieveAbstractAccounts.js deleted file mode 100644 index f053e016..00000000 --- a/src/common/managesieve.ui/accounts/SieveAbstractAccounts.js +++ /dev/null @@ -1,60 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - /* global SieveAccountUI */ - /* global SieveIpcClient */ - /* global SieveLogger */ - - /** - * A UI renderer for a list of sieve accounts - **/ - class SieveAbstractAccounts { - - /** - * Gets an instance to the logger. - * - * @returns {SieveLogger} - * an reference to the logger instance. - **/ - getLogger() { - return SieveLogger.getInstance(); - } - - /** - * Renders the UI for this component. - */ - async render() { - - this.getLogger().logWidget("Rendering Accounts..."); - - const items = document.querySelector(".siv-accounts-items"); - while (items.firstChild) - items.removeChild(items.firstChild); - - const accounts = await SieveIpcClient.sendMessage("core", "accounts-list"); - - for (const account of accounts) { - this.getLogger().logWidget(` + Accounts ${account}`); - await ((new SieveAccountUI(this, account)).render()); - } - } - } - - if (typeof (module) !== "undefined" && module && module.exports) - module.exports.SieveAbstractAccounts = SieveAbstractAccounts; - else - exports.SieveAbstractAccounts = SieveAbstractAccounts; - -})(this); diff --git a/src/common/managesieve.ui/accounts/SieveAbstractAccounts.mjs b/src/common/managesieve.ui/accounts/SieveAbstractAccounts.mjs new file mode 100644 index 00000000..5d27120a --- /dev/null +++ b/src/common/managesieve.ui/accounts/SieveAbstractAccounts.mjs @@ -0,0 +1,51 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +import { SieveAccountUI } from "./SieveAccountUI.mjs"; +import { SieveIpcClient } from "./../utils/SieveIpcClient.mjs"; +import { SieveLogger } from "./../utils/SieveLogger.mjs"; + +/** + * A UI renderer for a list of sieve accounts + **/ +class SieveAbstractAccounts { + + /** + * Gets an instance to the logger. + * + * @returns {SieveLogger} + * an reference to the logger instance. + **/ + getLogger() { + return SieveLogger.getInstance(); + } + + /** + * Renders the UI for this component. + */ + async render() { + + this.getLogger().logWidget("Rendering Accounts..."); + + const items = document.querySelector(".siv-accounts-items"); + while (items.firstChild) + items.firstChild.remove(); + + const accounts = await SieveIpcClient.sendMessage("core", "accounts-list"); + + for (const account of accounts) { + this.getLogger().logWidget(` + Accounts ${account}`); + await ((new SieveAccountUI(this, account)).render()); + } + } +} + +export { SieveAbstractAccounts }; diff --git a/src/common/managesieve.ui/accounts/SieveAccountUI.js b/src/common/managesieve.ui/accounts/SieveAccountUI.js deleted file mode 100644 index ec2db189..00000000 --- a/src/common/managesieve.ui/accounts/SieveAccountUI.js +++ /dev/null @@ -1,412 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - /* global bootstrap */ - - /* global SieveLogger */ - /* global SieveTemplate */ - /* global SieveScriptUI */ - /* global SieveIpcClient */ - /* global SieveServerSettingsUI */ - /* global SieveCredentialsSettingsUI */ - /* global SieveDebugSettingsUI */ - /* global SieveCapabilities */ - - const IS_SMALLER = -1; - const IS_EQUAL = 0; - const IS_LARGER = 1; - - /** - * A UI renderer for a sieve account - */ - class SieveAccountUI { - - /** - * Creates a new renderer for a sieve account. - * - * @param {SieveAccountsUI} accounts - * the parent sieve accounts renderer - * @param {string} id - * the account unique id. - */ - constructor(accounts, id) { - this.accounts = accounts; - this.id = id; - } - - /** - * Gets an instance to the logger. - * - * @returns {SieveLogger} - * an reference to the logger instance. - **/ - getLogger() { - return SieveLogger.getInstance(); - } - - /** - * Executes an action on the communication process. - * - * @param {string} action - * the actions unique name - * @param {object} [payload] - * the payload which should be send - * - * @returns {Promise<object>} - * the result received for this action. - */ - async send(action, payload) { - - if (typeof (payload) === "undefined" || payload === null) - payload = {}; - - if (typeof (payload) !== "object") - payload = { "data": payload }; - - payload["account"] = this.id; - - return await SieveIpcClient.sendMessage("core", action, payload); - } - - /** - * Checks if the current account has an active connection to the server - * - * @returns {boolean} true in case tha account is connected otherwise false - */ - async isConnected() { - return await this.send("account-connected"); - } - - /** - * Establishes a connection to the server - */ - async connect() { - - const item = await (new SieveTemplate()).load(`./accounts/account.connecting.tpl`); - - const scripts = document.querySelector(`#siv-account-${this.id} .siv-tpl-scripts`); - while (scripts.firstChild) - scripts.removeChild(scripts.firstChild); - - scripts.appendChild(item); - - try { - await this.send("account-connect"); - } - catch (ex) { - await this.disconnect(); - } - - await this.render(); - } - - /** - * Disconnects the account from the server. - */ - async disconnect() { - const item = await (new SieveTemplate()).load(`./accounts/account.disconnecting.tpl`); - - const scripts = document.querySelector(`#siv-account-${this.id} .siv-tpl-scripts`); - while (scripts.firstChild) - scripts.removeChild(scripts.firstChild); - - scripts.appendChild(item); - - await this.send("account-disconnect"); - await this.render(); - } - - /** - * Renders the settings pane - * - */ - async renderSettings() { - - const elm = await (new SieveTemplate()).load("./accounts/account.settings.tpl"); - - - const account = await this.send("account-get-settings"); - - - elm.querySelector(".sieve-settings-hostname") - .textContent = account.hostname; - elm.querySelector(".sieve-settings-port") - .textContent = account.port; - - if (!account.secure) - elm.querySelector(".sieve-settings-secure").style.display = 'none'; - - elm.querySelector(".sieve-settings-username") - .textContent = account.username; - - if (elm.querySelector(".sieve-settings-mechanism")) { - elm.querySelector(".sieve-settings-mechanism") - .textContent = account.mechanism; - } - - if (elm.querySelector(".sieve-settings-fingerprint")) { - elm.querySelector(".sieve-settings-fingerprint") - .textContent = account.fingerprint; - - if (account.fingerprint !== "") - elm.querySelector(".sieve-settings-fingerprint-item").classList.remove("d-none"); - } - - // Clear any existing left overs... - const settings = document.querySelector(`#siv-account-${this.id} .sieve-settings-content`); - while (settings.firstChild) - settings.removeChild(settings.firstChild); - - // ... and append the new element - settings.appendChild(elm); - - // ... finally connect the listeners. - if (elm.querySelector(".sieve-account-delete-server")) { - elm.querySelector(".sieve-account-delete-server") - .addEventListener("click", () => { this.remove(); }); - } - - if (elm.querySelector(".sieve-account-edit-server")) { - elm.querySelector(".sieve-account-edit-server") - .addEventListener("click", () => { this.showServerSettings(); }); - } - - if (elm.querySelector(".sieve-account-edit-credentials")) { - elm.querySelector(".sieve-account-edit-credentials") - .addEventListener("click", () => { this.showCredentialSettings(); }); - } - - if (elm.querySelector(".sieve-account-edit-debug")) { - elm.querySelector(".sieve-account-edit-debug") - .addEventListener("click", () => { this.showAdvancedSettings(); }); - } - - if (elm.querySelector(".sieve-account-export")) { - elm.querySelector(".sieve-account-export") - .addEventListener("click", () => { this.exportSettings(); }); - } - - } - - /** - * Renders the accounts outer ui - * - */ - async renderAccount() { - const elm = await (new SieveTemplate()).load("./accounts/account.tpl"); - - elm.id = `siv-account-${this.id}`; - - elm.querySelector(".sieve-accounts-content").id = `sieve-accounts-content-${this.id}`; - elm.querySelector(".sieve-accounts-tab").href = `#sieve-accounts-content-${this.id}`; - - elm.querySelector(".sieve-settings-content").id = `sieve-settings-content-${this.id}`; - elm.querySelector(".sieve-settings-tab").href = `#sieve-settings-content-${this.id}`; - elm.querySelector(".sieve-settings-tab").addEventListener('shown.bs.tab', () => { this.renderSettings(); }); - - elm.querySelector(".siv-account-name").textContent - = await this.send("account-get-displayname"); - - document.querySelector(".siv-accounts-items").appendChild(elm); - - elm - .querySelector(".siv-account-create") - .addEventListener("click", () => { this.createScript(); }); - elm - .querySelector(".sieve-account-edit-settings") - .addEventListener("click", () => { this.showSettings(); }); - elm - .querySelector(".sieve-account-capabilities") - .addEventListener("click", () => { this.showCapabilities(); }); - elm - .querySelector(".sieve-account-reconnect-server") - .addEventListener("click", () => { this.connect(); }); - elm - .querySelector(".sieve-account-disconnect-server") - .addEventListener("click", () => { this.disconnect(); }); - } - - /** - * Renders the account ui's script pane - */ - async onRenderConnected() { - const data = await this.send("account-list"); - - const scripts = document.querySelector(`#siv-account-${this.id} .siv-tpl-scripts`); - - while (scripts.firstChild) - scripts.removeChild(scripts.firstChild); - - if (!data.length) { - const elm = await (new SieveTemplate()).load(`./accounts/account.empty.html`); - scripts.appendChild(elm); - - elm - .querySelector(".sieve-script-empty-create") - .addEventListener("click", () => { this.createScript(); }); - } - - // Sort the script names by their name... - data.sort((a, b) => { - const scriptA = a.script.toUpperCase(); - const scriptB = b.script.toUpperCase(); - - if (scriptA < scriptB) - return IS_SMALLER; - - if (scriptA > scriptB) - return IS_LARGER; - - return IS_EQUAL; - }); - - data.forEach(async (item) => { - this.getLogger().logWidget(`Rendering ${this.id}/${item.script}`); - await ((new SieveScriptUI(this, item.script, item.active)).render()); - }); - - } - - /** - * Called when the account should be rendered because of a disconnect. - */ - async onRenderDisconnected() { - - const elm = await (new SieveTemplate()).load(`./accounts/account.disconnected.tpl`); - - const scripts = document.querySelector(`#siv-account-${this.id} .siv-tpl-scripts`); - - while (scripts.firstChild) - scripts.removeChild(scripts.firstChild); - - scripts.appendChild(elm); - - elm.querySelector(".sieve-script-connect") - .addEventListener("click", () => { this.connect(); }); - } - - /** - * Renders the UI for this component. - */ - async render() { - this.getLogger().logWidget(`Rendering Account ${this.id}`); - - if (!document.querySelector(`#siv-account-${this.id}`)) { - await this.renderAccount(); - this.renderSettings(); - } - - const status = await this.isConnected(); - - if (status === false) { - await this.onRenderDisconnected("disconnected"); - return; - } - - this.onRenderConnected(); - } - - /** - * Asks the user if he is sure to delete the account. - * If yes it triggers expunging the account settings. - * This can not be undone. - */ - async remove() { - await this.accounts.remove(this); - } - - /** - * Shows the settings dialog - */ - showSettings() { - const tab = document.querySelector(`#siv-account-${this.id} .sieve-settings-tab`); - (new bootstrap.Tab(tab)).show(); - } - - /** - * Shows the server settings dialog. - */ - async showServerSettings() { - - await (new SieveServerSettingsUI(this)).show(); - - this.renderSettings(); - - // Update the account name it may have changed. - document - .querySelector(`#siv-account-${this.id} .siv-account-name`) - .textContent = await this.send("account-get-displayname"); - } - - /** - * Shows the credential settings dialog. - **/ - showCredentialSettings() { - (new SieveCredentialsSettingsUI(this)).show(); - } - - /** - * Show the advanced settings dialog - */ - showAdvancedSettings() { - (new SieveDebugSettingsUI(this)).show(); - } - - /** - * Exports the account's settings to a file. - */ - async exportSettings() { - await this.send("account-export"); - } - - /** - * Shows the account's capabilities - * - * @returns {SieveAccountUI} - * a self reference. - */ - async showCapabilities() { - - if (await this.isConnected() === false) - return this; - - const capabilities = await this.send("account-capabilities"); - await (new SieveCapabilities()).show(capabilities); - - return this; - } - - /** - * Prompts for the new script name an creates the script - */ - async createScript() { - - if (await this.isConnected() === false) - return; - - const name = await this.send("script-create"); - - if (name !== "") - await this.render(); - } - - } - - if (typeof (module) !== "undefined" && module !== null && module.exports) - module.exports = SieveAccountUI; - else - exports.SieveAccountUI = SieveAccountUI; - -})(this); diff --git a/src/common/managesieve.ui/accounts/SieveCapabilities.js b/src/common/managesieve.ui/accounts/SieveCapabilities.js deleted file mode 100644 index 7d318ab1..00000000 --- a/src/common/managesieve.ui/accounts/SieveCapabilities.js +++ /dev/null @@ -1,71 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - /* global bootstrap */ - /* global SieveTemplate */ - - /** - * Implements a dialog which displays the accounts capabilities. - */ - class SieveCapabilities { - - /** - * Shows the capability dialog. - * - * @param {object} capabilities - * the server's capabilities. - */ - async show(capabilities) { - - document.querySelector("#ctx").appendChild( - await (new SieveTemplate()).load("./accounts/account.capabilities.tpl")); - - document.querySelector("#sieve-capabilities-server").textContent - = capabilities.implementation; - document.querySelector("#sieve-capabilities-version").textContent - = capabilities.version; - document.querySelector("#sieve-capabilities-sasl").textContent - = Object.values(capabilities.sasl).join(" "); - document.querySelector("#sieve-capabilities-extensions").textContent - = Object.keys(capabilities.extensions).join(" "); - document.querySelector("#sieve-capabilities-language").textContent - = capabilities.language; - - await new Promise((resolve) => { - - const dialog = document.querySelector('#sieve-dialog-capabilities'); - - const modal = new bootstrap.Modal(dialog); - modal.show(); - - dialog.addEventListener("hidden.bs.modal", () => { - resolve(); - - const elm = document.querySelector('#sieve-dialog-capabilities'); - elm.parentNode.removeChild(elm); - - modal.dispose(); - }); - }); - - } - } - - if (typeof (module) !== "undefined" && module && module.exports) - module.exports.SieveCapabilities = SieveCapabilities; - else - exports.SieveCapabilities = SieveCapabilities; - -})(this); diff --git a/src/common/managesieve.ui/accounts/SieveCapabilities.mjs b/src/common/managesieve.ui/accounts/SieveCapabilities.mjs new file mode 100644 index 00000000..8d2c0c1d --- /dev/null +++ b/src/common/managesieve.ui/accounts/SieveCapabilities.mjs @@ -0,0 +1,62 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +/* global bootstrap */ +import { SieveTemplate } from "./../utils/SieveTemplate.mjs"; + +/** + * Implements a dialog which displays the accounts capabilities. + */ +class SieveCapabilities { + + /** + * Shows the capability dialog. + * + * @param {object} capabilities + * the server's capabilities. + */ + async show(capabilities) { + + document.querySelector("#ctx").append( + await (new SieveTemplate()).load("./accounts/account.capabilities.html")); + + document.querySelector("#sieve-capabilities-server").textContent + = capabilities.implementation; + document.querySelector("#sieve-capabilities-version").textContent + = capabilities.version; + document.querySelector("#sieve-capabilities-sasl").textContent + = Object.values(capabilities.sasl).join(" "); + document.querySelector("#sieve-capabilities-extensions").textContent + = Object.keys(capabilities.extensions).join(" "); + document.querySelector("#sieve-capabilities-language").textContent + = capabilities.language; + + await new Promise((resolve) => { + + const dialog = document.querySelector('#sieve-dialog-capabilities'); + + const modal = new bootstrap.Modal(dialog); + modal.show(); + + dialog.addEventListener("hidden.bs.modal", () => { + resolve(); + + const elm = document.querySelector('#sieve-dialog-capabilities'); + elm.remove(); + + modal.dispose(); + }); + }); + + } +} + +export { SieveCapabilities }; diff --git a/src/common/managesieve.ui/accounts/SieveScriptUI.tpl b/src/common/managesieve.ui/accounts/SieveScriptUI.html index 296c5cc1..0788404d 100644 --- a/src/common/managesieve.ui/accounts/SieveScriptUI.tpl +++ b/src/common/managesieve.ui/accounts/SieveScriptUI.html @@ -3,11 +3,11 @@ <span class="sieve-list-script-name"></span> <span class="sieve-list-script-active badge bg-success" data-i18n="account.script.active"></span> </div> - <div class="float-right"> - <button class="btn btn-sm btn-outline-secondary mr-1 sieve-script-deactivate" data-i18n="account.script.deactivate"></button> - <button class="btn btn-sm btn-outline-secondary mr-1 sieve-script-activate" data-i18n="account.script.activate"></button> - <button class="btn btn-sm btn-outline-secondary mr-1 sieve-script-rename" data-i18n="account.script.rename"></button> - <button class="btn btn-sm btn-outline-secondary mr-1 sieve-script-delete" data-i18n="account.script.delete"></button> - <button class="btn btn-sm btn-outline-secondary mr-1 sieve-script-edit" data-i18n="account.script.edit"></button> + <div class="float-end"> + <button class="btn btn-sm btn-outline-secondary me-1 sieve-script-deactivate" data-i18n="account.script.deactivate"></button> + <button class="btn btn-sm btn-outline-secondary me-1 sieve-script-activate" data-i18n="account.script.activate"></button> + <button class="btn btn-sm btn-outline-secondary me-1 sieve-script-rename" data-i18n="account.script.rename"></button> + <button class="btn btn-sm btn-outline-secondary me-1 sieve-script-delete" data-i18n="account.script.delete"></button> + <button class="btn btn-sm btn-outline-secondary me-1 sieve-script-edit" data-i18n="account.script.edit"></button> </div> </li>
\ No newline at end of file diff --git a/src/common/managesieve.ui/accounts/SieveScriptUI.js b/src/common/managesieve.ui/accounts/SieveScriptUI.js deleted file mode 100644 index c4b190c3..00000000 --- a/src/common/managesieve.ui/accounts/SieveScriptUI.js +++ /dev/null @@ -1,156 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - /* global SieveTemplate */ - - "use strict"; - - const HEX = 16; - const HEX_OFFSET = -2; - - /** - * An UI elements which handles displaying details for a sieve script. - * It does not provide any support for editing the scripts content. - */ - class SieveScriptUI { - - /** - * Creates a new instance - * @param {SieveAccount} account - * the account UI element which owns this script - * @param {string} name - * the scripts name - * @param {boolean} active - * indicates if the script should be rendered as active. - */ - constructor(account, name, active) { - this.name = name; - this.isActive = active; - this.account = account; - } - - /** - * Returns a unique id for this script. - * It is the account id concatenated to the script name in hex. - * - * @returns {string} - * the unique id which identifies this element.. - */ - getId() { - // Convert the name into hex to escape dangerous characters. - let str = ""; - for (let i = 0; i < this.name.length; i++) - str += ("0" + this.name.charCodeAt(i).toString(HEX)).slice(HEX_OFFSET); - - return `${this.account.id}-${str}`; - } - - - /** - * Renders the UI element into the dom. - */ - async render() { - - const id = this.getId(); - - let elm = document.querySelector(`#siv-script-${id}`); - // Check if the element exists... - if (!elm) { - - elm = await (new SieveTemplate()).load("./accounts/SieveScriptUI.tpl"); - - elm.id = `siv-script-${id}`; - - document - .querySelector(`#siv-account-${this.account.id} .siv-tpl-scripts`) - .appendChild(elm); - - elm.querySelector(".sieve-list-script-name").textContent = this.name; - - elm.querySelector(".sieve-script-rename") - .addEventListener("click", () => { this.rename(); }); - elm.querySelector(".sieve-script-delete") - .addEventListener("click", () => { this.remove(); }); - elm.querySelector(".sieve-script-edit") - .addEventListener("click", () => { this.edit(); }); - elm.querySelector(".sieve-script-activate") - .addEventListener("click", () => { this.activate(); }); - elm.querySelector(".sieve-script-deactivate") - .addEventListener("click", () => { this.deactivate(); }); - } - - if (this.isActive === false) { - elm.querySelector(".sieve-list-script-active").classList.add("d-none"); - elm.querySelector(".sieve-script-activate").classList.remove("d-none"); - elm.querySelector(".sieve-script-deactivate").classList.add("d-none"); - } - else { - elm.querySelector(".sieve-list-script-active").classList.remove("d-none"); - elm.querySelector(".sieve-script-activate").classList.add("d-none"); - elm.querySelector(".sieve-script-deactivate").classList.remove("d-none"); - } - } - - /** - * Renames the script. - * A prompt will be show which ask the user about the new name - */ - async rename() { - - const rv = await this.account.send("script-rename", this.name); - if (rv === true) - this.account.render(); - } - - /** - * Removes the script - * A verification prompt will be shown before the script is deleted - */ - async remove() { - const rv = await this.account.send("script-delete", this.name); - if (rv === true) - await this.account.render(); - } - - /** - * Open the script in a new tab. In order to edit it. - */ - async edit() { - await this.account.send("script-edit", this.name); - } - - /** - * Marks the script as active. - */ - async activate() { - await this.account.send("script-activate", this.name); - await this.account.render(); - } - - /** - * Marks the script as in active. - */ - async deactivate() { - await this.account.send("script-deactivate", this.name); - await this.account.render(); - } - - } - - - if (typeof (module) !== "undefined" && module !== null && module.exports) - module.exports = SieveScriptUI; - else - exports.SieveScriptUI = SieveScriptUI; - -})(this); diff --git a/src/common/managesieve.ui/accounts/SieveScriptUI.mjs b/src/common/managesieve.ui/accounts/SieveScriptUI.mjs new file mode 100644 index 00000000..c3c0618a --- /dev/null +++ b/src/common/managesieve.ui/accounts/SieveScriptUI.mjs @@ -0,0 +1,146 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +import { SieveTemplate } from "./../utils/SieveTemplate.mjs"; + +const HEX = 16; +const HEX_OFFSET = -2; + +/** + * An UI elements which handles displaying details for a sieve script. + * It does not provide any support for editing the scripts content. + */ +class SieveScriptUI { + + /** + * Creates a new instance + * @param {SieveAccount} account + * the account UI element which owns this script + * @param {string} name + * the scripts name + * @param {boolean} active + * indicates if the script should be rendered as active. + */ + constructor(account, name, active) { + this.name = name; + this.isActive = active; + this.account = account; + } + + /** + * Returns a unique id for this script. + * It is the account id concatenated to the script name in hex. + * + * @returns {string} + * the unique id which identifies this element.. + */ + getId() { + // Convert the name into hex to escape dangerous characters. + let str = ""; + for (let i = 0; i < this.name.length; i++) + str += ("0" + this.name.charCodeAt(i).toString(HEX)).slice(HEX_OFFSET); + + return `${this.account.id}-${str}`; + } + + + /** + * Renders the UI element into the dom. + */ + async render() { + + const id = this.getId(); + + let elm = document.querySelector(`#siv-script-${id}`); + // Check if the element exists... + if (!elm) { + + elm = await (new SieveTemplate()).load("./accounts/SieveScriptUI.html"); + + elm.id = `siv-script-${id}`; + + document + .querySelector(`#siv-account-${this.account.id} .siv-tpl-scripts`) + .append(elm); + + elm.querySelector(".sieve-list-script-name").textContent = this.name; + + elm.querySelector(".sieve-script-rename") + .addEventListener("click", () => { this.rename(); }); + elm.querySelector(".sieve-script-delete") + .addEventListener("click", () => { this.remove(); }); + elm.querySelector(".sieve-script-edit") + .addEventListener("click", () => { this.edit(); }); + elm.querySelector(".sieve-script-activate") + .addEventListener("click", () => { this.activate(); }); + elm.querySelector(".sieve-script-deactivate") + .addEventListener("click", () => { this.deactivate(); }); + } + + if (this.isActive === false) { + elm.querySelector(".sieve-list-script-active").classList.add("d-none"); + elm.querySelector(".sieve-script-activate").classList.remove("d-none"); + elm.querySelector(".sieve-script-deactivate").classList.add("d-none"); + } + else { + elm.querySelector(".sieve-list-script-active").classList.remove("d-none"); + elm.querySelector(".sieve-script-activate").classList.add("d-none"); + elm.querySelector(".sieve-script-deactivate").classList.remove("d-none"); + } + } + + /** + * Renames the script. + * A prompt will be show which ask the user about the new name + */ + async rename() { + + const rv = await this.account.send("script-rename", this.name); + if (rv === true) + this.account.render(); + } + + /** + * Removes the script + * A verification prompt will be shown before the script is deleted + */ + async remove() { + const rv = await this.account.send("script-delete", this.name); + if (rv === true) + await this.account.render(); + } + + /** + * Open the script in a new tab. In order to edit it. + */ + async edit() { + await this.account.send("script-edit", this.name); + } + + /** + * Marks the script as active. + */ + async activate() { + await this.account.send("script-activate", this.name); + await this.account.render(); + } + + /** + * Marks the script as in active. + */ + async deactivate() { + await this.account.send("script-deactivate", this.name); + await this.account.render(); + } + +} + +export { SieveScriptUI }; diff --git a/src/common/managesieve.ui/accounts/account.capabilities.tpl b/src/common/managesieve.ui/accounts/account.capabilities.html index dc21bdd4..56396de3 100644 --- a/src/common/managesieve.ui/accounts/account.capabilities.tpl +++ b/src/common/managesieve.ui/accounts/account.capabilities.html @@ -3,7 +3,7 @@ <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" data-i18n="account.capabilities.title"></h5> - <button type="button" class="btn-close" data-dismiss="modal" aria-label="Close"></button> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> <table class="table-sm"> diff --git a/src/common/managesieve.ui/accounts/account.connecting.tpl b/src/common/managesieve.ui/accounts/account.connecting.html index aed186da..aed186da 100644 --- a/src/common/managesieve.ui/accounts/account.connecting.tpl +++ b/src/common/managesieve.ui/accounts/account.connecting.html diff --git a/src/common/managesieve.ui/accounts/account.disconnected.tpl b/src/common/managesieve.ui/accounts/account.disconnected.html index 92febd67..efd4cf46 100644 --- a/src/common/managesieve.ui/accounts/account.disconnected.tpl +++ b/src/common/managesieve.ui/accounts/account.disconnected.html @@ -3,5 +3,5 @@ <h4 class="card-title" data-i18n="account.disconnected.title"></h4> <p class="card-text" data-i18n="account.disconnected.description"></p> <button data-i18n="account.connect" - type="button" class="btn btn-outline-primary mr-1 sieve-script-connect"></button> + type="button" class="btn btn-outline-primary me-1 sieve-script-connect"></button> </div> diff --git a/src/common/managesieve.ui/accounts/account.disconnecting.tpl b/src/common/managesieve.ui/accounts/account.disconnecting.html index d88d2539..d88d2539 100644 --- a/src/common/managesieve.ui/accounts/account.disconnecting.tpl +++ b/src/common/managesieve.ui/accounts/account.disconnecting.html diff --git a/src/common/managesieve.ui/accounts/account.empty.html b/src/common/managesieve.ui/accounts/account.empty.html index 52fdc75d..02f4434c 100644 --- a/src/common/managesieve.ui/accounts/account.empty.html +++ b/src/common/managesieve.ui/accounts/account.empty.html @@ -2,5 +2,5 @@ <h4 class="card-title" data-i18n="account.empty.title"></h4> <p class="card-text" data-i18n="account.empty.description"></p> <button data-i18n="account.empty.create" - type="button" class="btn btn-outline-primary mr-1 sieve-script-empty-create"></button> + type="button" class="btn btn-outline-primary me-1 sieve-script-empty-create"></button> </div>
\ No newline at end of file diff --git a/src/common/managesieve.ui/accounts/account.tpl b/src/common/managesieve.ui/accounts/account.html index 5f67a1d3..e966e2df 100644 --- a/src/common/managesieve.ui/accounts/account.tpl +++ b/src/common/managesieve.ui/accounts/account.html @@ -4,26 +4,26 @@ <div class="card-header d-flex justify-content-between py-0"> <ul class="nav nav-tabs card-header-tabs my-0 pt-3" role="tablist"> <li class="nav-item"> - <a class="sieve-accounts-tab nav-link active" data-toggle="tab" role="tab"> + <a class="sieve-accounts-tab nav-link active" data-bs-toggle="tab" role="tab"> <span class="siv-account-name"></span> </a> </li> <li class="nav-item"> - <a class="sieve-settings-tab nav-link" data-i18n="account.settings" data-toggle="tab" role="tab"></a> + <a class="sieve-settings-tab nav-link" data-i18n="account.settings" data-bs-toggle="tab" role="tab"></a> </li> </ul> <div class="align-self-center"> - <button type="button" class="btn btn-sm btn-outline-secondary mr-1 siv-account-create" data-i18n="account.newscript"></button> - <a class="btn btn-sm btn-outline-info mr-1 " + <button type="button" class="btn btn-sm btn-outline-secondary me-1 siv-account-create" data-i18n="account.newscript"></button> + <a class="btn btn-sm btn-outline-info me-1 " href="https://www.paypal.com/paypalme2/thsmi" target="_blank" role="button" data-i18n="account.donate"></a> <div id="sieve-editor-settings" class="btn-group dropdown"> - <button class="btn btn-sm btn-outline-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" + <button class="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> ☰ </button> - <div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuLink"> + <div class="dropdown-menu dropdown-menu-end" aria-labelledby="dropdownMenuLink"> <a class="dropdown-item sieve-account-edit-settings" data-i18n="account.settings.edit"></a> <div class="dropdown-divider"></div> <a class="dropdown-item sieve-account-disconnect-server" data-i18n="account.disconnect"></a> diff --git a/src/common/managesieve.ui/dialogs/SieveDialogUI.js b/src/common/managesieve.ui/dialogs/SieveDialogUI.js deleted file mode 100644 index a517723d..00000000 --- a/src/common/managesieve.ui/dialogs/SieveDialogUI.js +++ /dev/null @@ -1,706 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - const KEY_RETURN = 13; - - const DIALOG_CANCELED = 0; - const DIALOG_ACCEPTED = 1; - const DIALOG_DISCARDED = 2; - - /* global bootstrap */ - const { SieveTemplate } = require("./../utils/SieveTemplate.js"); - const { SieveUniqueId } = require("./../utils/SieveUniqueId.js"); - - /** - * Displays a simple dialog with an action button. - */ - class SieveDialog { - - /** - * The path to the html template which should be used for this dialog - * @abstract - * - * @returns {string} - * the path to the html template. - */ - getTemplate() { - throw new Error("Implement getTemplate"); - } - - /** - * Get the current dialogs root element - * - * @returns {HTMLElement} - * the dialogs root element. - */ - getDialog() { - return document.querySelector(`#${this.id}`); - } - - /** - * Called when the dialog is initialized. - * It is used to populate it with html elements. - * - * You can use getDialog to retrieve the dialogs root element. - * E.g. when adding new elements. - * - */ - onInit() { - throw new Error("Implement on Init"); - } - - /** - * Called when the user clicks the accept button. - * Which is any button marked with the "sieve-dialog-resolve" class. - * - * The result should be typically true for simple dialogs or - * with more complex dialogs just return the desired value. - * - * @returns {object} - * returns true or the accept value - * - */ - onAccept() { - return true; - } - - /** - * Called when the user clicks the cancel button. - * Which is any button marked with the "sieve-dialog-reject" class. - * - * The result should be false for simple dialogs or a default value for - * more complex dialogs. In case there is no default value, the best strategy - * is to throw an exception. - * - * @returns {object} - * returns false or the reject result. - */ - onCancel() { - return false; - } - - /** - * Called when the dialog is shown. - * This can be used e.g. to move the focus to the desired text box. - */ - onShown() { - } - - /** - * Generates an if made of alphanumerical characters and dashes. - * @returns {string} - * a string with an html compatible unique id - */ - generateId() { - return (new SieveUniqueId()).generate(); - } - - /** - * Removes the dialog window from the UI. - */ - destroy() { - const elm = this.getDialog(); - elm.parentNode.removeChild(elm); - } - - /** - * Shows the dialog. - * - * @returns {object} - * the value returned from the dialog. - */ - async show() { - - this.id = this.generateId(); - - const dialog = await (new SieveTemplate()).load(this.getTemplate()); - dialog.id = this.id; - document.querySelector("#ctx").appendChild(dialog); - - this.onInit(); - - return await new Promise((resolve, reject) => { - - const modal = new bootstrap.Modal(this.getDialog()); - - const buttons = this.getDialog() - .querySelectorAll(".sieve-dialog-resolve"); - - for (const button of buttons) { - button.addEventListener("click", async () => { - try { - resolve(await this.onAccept(event.target)); - } catch (ex) { - reject(ex); - } - - modal.hide(); - }); - } - - - modal.show(); - - this.getDialog().addEventListener('hidden.bs.modal', async () => { - - this.destroy(); - - try { - resolve(await this.onCancel()); - } catch (ex) { - reject(ex); - } - }); - - this.getDialog().addEventListener('shown.bs.modal', () => { - this.onShown(); - }); - }); - } - } - - /** - * Prompts if the given account shall be deleted. - */ - class SieveDeleteAccountDialog extends SieveDialog { - - /** - * Creates a new dialog instance - * - * @param {string} displayName - * the accounts display name - */ - constructor(displayName) { - super(); - this.displayName = displayName; - } - - /** - * @inheritdoc - */ - getTemplate() { - return "./dialogs/dialog.account.delete.tpl"; - } - - /** - * @inheritdoc - */ - onInit() { - this.getDialog() - .querySelector("#sieve-dialog-account-remove-name") - .textContent = this.displayName; - } - } - - /** - * Shows the dialog asking for the server's fingerprint. - */ - class SieveFingerprintDialog extends SieveDialog { - - /** - * Creates a new instance - * - * @param {object} secInfo - * the security info object with details about the validation error. - */ - constructor(secInfo) { - super(); - this.fingerprint = secInfo.fingerprint; - this.error = secInfo.message; - } - - /** - * @inheritdoc - */ - getTemplate() { - return "./dialogs/dialog.account.cert.tpl"; - } - - /** - * @inheritdoc - */ - onInit() { - this.getDialog() - .querySelector(".sieve-dialog-fingerprint") - .textContent = this.fingerprint; - - this.getDialog() - .querySelector(".sieve-dialog-certerror") - .textContent = this.error; - } - } - - /** - * Aks if the given script should be deleted or not. - */ - class SieveDeleteScriptDialog extends SieveDialog { - - /** - * Create a new delete script dialog instance. - * - * @param {string} name - * the script name as string - */ - constructor(name) { - super(); - this.name = name; - } - - /** - * @inheritdoc - */ - getTemplate() { - return "./dialogs/dialog.script.delete.tpl"; - } - - /** - * @inheritdoc - */ - onInit() { - this.getDialog() - .querySelector('.sieve-delete-dialog-name') - .textContent = this.name; - } - } - - /** - * Asks for a name, which should be used to create the new script. - */ - class SieveCreateScriptDialog extends SieveDialog { - - /** - * @inheritdoc - */ - getTemplate() { - return "./dialogs/dialog.script.create.tpl"; - } - - /** - * @inheritdoc - */ - onInit() { - this.getDialog().querySelector('.sieve-create-dialog-name').addEventListener("keypress", (e) => { - if (e.which === KEY_RETURN) { - const event = document.createEvent('HTMLEvents'); - event.initEvent('click', true, false); - this.getDialog().querySelector(".sieve-dialog-resolve").dispatchEvent(event); - } - }); - } - - /** - * @inheritdoc - */ - onShown() { - this.getDialog().querySelector('.sieve-create-dialog-name').focus(); - } - - /** - * @inheritdoc - */ - onAccept() { - return this.getDialog().querySelector('.sieve-create-dialog-name').value; - } - - /** - * @inheritdoc - */ - onCancel() { - return ""; - } - } - - /** - * Asks for a scripts new name - */ - class SieveRenameScriptDialog extends SieveDialog { - - /** - * Create a rename script dialog instance. - * @param {string} name - * the scripts old name - */ - constructor(name) { - super(); - this.name = name; - } - - /** - * @inheritdoc - */ - getTemplate() { - return "./dialogs/dialog.script.rename.tpl"; - } - - /** - * @inheritdoc - */ - onInit() { - this.getDialog().querySelector('.sieve-rename-dialog-newname').addEventListener("keypress", (e) => { - if (e.which === KEY_RETURN) { - const event = document.createEvent('HTMLEvents'); - event.initEvent('click', true, false); - this.getDialog().querySelector(".sieve-dialog-resolve").dispatchEvent(event); - } - }); - - this.getDialog() - .querySelector(".sieve-rename-dialog-newname").value = this.name; - } - - /** - * @inheritdoc - */ - onShown() { - this.getDialog().querySelector('.sieve-rename-dialog-newname').focus(); - } - - /** - * @inheritdoc - */ - onAccept() { - return this.getDialog() - .querySelector(".sieve-rename-dialog-newname").value; - } - - /** - * @inheritdoc - */ - onCancel() { - return this.name; - } - } - - /** - * Asks for a password. - * The show method returns null or the authorization. - */ - class SievePasswordDialog extends SieveDialog { - - /** - * Creates a password dialog. - * - * @param {string} username - * the username for which the password is requested - * @param {string} displayName - * the accounts display name. - * @param {{ remember : boolean }} [options] - * extended additional options. - * In case "remember" is set to true a switch will be rendered which allows - * the user to select if the password should be stored. - */ - constructor(username, displayName, options) { - super(); - this.username = username; - this.displayName = displayName; - - if (typeof(options) === "undefined" || options === null) - options = {}; - - this.options = options; - } - - /** - * @inheritdoc - */ - getTemplate() { - return "./dialogs/dialog.account.password.tpl"; - } - - /** - * @inheritdoc - */ - onInit() { - const dialog = this.getDialog(); - - if (!this.options.remember) - dialog.querySelector(".sieve-password-remember").style.display = "none"; - - dialog.querySelector(".sieve-username").textContent = this.username; - dialog.querySelector(".sieve-displayname").textContent = this.displayName; - - this.getDialog().querySelector('.sieve-password').addEventListener("keypress", (e) => { - if (e.which === KEY_RETURN) { - const event = document.createEvent('HTMLEvents'); - event.initEvent('click', true, false); - this.getDialog().querySelector(".sieve-dialog-resolve").dispatchEvent(event); - } - }); - } - - /** - * @inheritdoc - */ - onShown() { - this.getDialog().querySelector('.sieve-password').focus(); - } - - /** - * @inheritdoc - */ - onAccept() { - return { - "username" : this.username, - "password" : this.getDialog().querySelector(".sieve-password").value, - "remember" : document.querySelector("#sieve-password-remember").checked - }; - } - - /** - * @inheritdoc - */ - onCancel() { - return {}; - } - } - - /** - * Asks for a authorization. - * The show method returns null or the authorization. - */ - class SieveAuthorizationDialog extends SieveDialog { - - /** - * Creates a authorization request dialog. - * - * @param {string} displayName - * the account's display name. - */ - constructor(displayName) { - super(); - this.displayName = displayName; - } - - /** - * @inheritdoc - */ - getTemplate() { - return "./dialogs/dialog.account.authorization.tpl"; - } - - /** - * @inheritdoc - */ - onInit() { - const dialog = this.getDialog(); - - dialog.querySelector(".sieve-displayname").textContent = this.displayName; - - dialog.querySelector('.sieve-authorization').addEventListener("keypress", (e) => { - if (e.which === KEY_RETURN) { - const event = document.createEvent('HTMLEvents'); - event.initEvent('click', true, false); - this.getDialog().querySelector(".sieve-dialog-resolve").dispatchEvent(event); - } - }); - } - - /** - * @inheritdoc - */ - onShown() { - this.getDialog().querySelector('.sieve-authorization').focus(); - } - - /** - * @inheritdoc - */ - onAccept() { - return this.getDialog().querySelector(".sieve-authorization").value; - } - - /** - * @inheritdoc - */ - onCancel() { - return null; - } - } - - /** - * An info dialog indicating the current script is busy. - */ - class SieveScriptBusyDialog extends SieveDialog { - - /** - * Create a new script busy dialog. - * @param {string} name - * the scripts name. - */ - constructor(name) { - super(); - this.name = name; - } - - /** - * @inheritdoc - */ - getTemplate() { - return "./dialogs/dialog.script.busy.tpl"; - } - - /** - * @inheritdoc - */ - onInit() { - this.getDialog() - .querySelector(".sieve-busy-dialog-scriptname").textContent = this.name; - } - } - - // TODO should be extracted an stored next to the editor - /** - * Asks is a changed script should be saved - */ - class SieveScriptSaveDialog extends SieveDialog { - - /** - * Create a rename script dialog instance. - * @param {string} name - * the scripts old name - */ - constructor(name) { - super(); - this.name = name; - } - - /** - * @inheritdoc - */ - getTemplate() { - return "./dialogs/dialog.script.save.tpl"; - } - - /** - * @inheritdoc - */ - onInit() { - this.getDialog() - .querySelector(".sieve-save-dialog-scriptname").textContent = this.name; - } - - /** - * @inheritdoc - */ - onAccept(element) { - if (element.classList.contains("sieve-save-dialog-discard")) - return DIALOG_DISCARDED; - - if (element.classList.contains("sieve-save-dialog-save")) - return DIALOG_ACCEPTED; - - throw new Error("Unknown button pressed"); - } - - /** - * Checks if the dialogs return value was a discard - * @param {int} value - * the value to be checked. - * @returns {boolean} - * true in case the dialog was discarded. - */ - static isDiscarded(value) { - return value === DIALOG_DISCARDED; - } - - /** - * Checks if the dialogs return value was a cancel - * @param {int} value - * the value to be checked. - * @returns {boolean} - * true in case the dialog was canceled - */ - static isCanceled(value) { - return value === DIALOG_CANCELED; - } - - /** - * Checks if the dialogs return value was an accept - * @param {int} value - * the value to be checked. - * @returns {boolean} - * true in case the dialog was accepted. - */ - static isAccepted(value) { - return value === DIALOG_ACCEPTED; - } - - /** - * @inheritdoc - */ - onCancel() { - return DIALOG_CANCELED; - } - } - - /** - * An info dialog indicating the current script is busy. - */ - class SieveErrorDialog extends SieveDialog { - - /** - * Create a new script busy dialog. - * @param {string} description - * the scripts name. - */ - constructor(description) { - super(); - this.description = description; - } - - /** - * @inheritdoc - */ - getTemplate() { - return "./dialogs/dialog.error.tpl"; - } - - /** - * @inheritdoc - */ - onInit() { - this.getDialog() - .querySelector(".sieve-error-dialog-description") - .textContent = this.description; - } - } - - - if (typeof (module) !== "undefined" && module && module.exports) { - module.exports.SievePasswordDialog = SievePasswordDialog; - module.exports.SieveRenameScriptDialog = SieveRenameScriptDialog; - module.exports.SieveCreateScriptDialog = SieveCreateScriptDialog; - module.exports.SieveDeleteScriptDialog = SieveDeleteScriptDialog; - module.exports.SieveFingerprintDialog = SieveFingerprintDialog; - module.exports.SieveDeleteAccountDialog = SieveDeleteAccountDialog; - module.exports.SieveAuthorizationDialog = SieveAuthorizationDialog; - module.exports.SieveScriptBusyDialog = SieveScriptBusyDialog; - module.exports.SieveScriptSaveDialog = SieveScriptSaveDialog; - module.exports.SieveErrorDialog = SieveErrorDialog; - } - else { - exports.SievePasswordDialog = SievePasswordDialog; - exports.SieveRenameScriptDialog = SieveRenameScriptDialog; - exports.SieveCreateScriptDialog = SieveCreateScriptDialog; - exports.SieveDeleteScriptDialog = SieveDeleteScriptDialog; - exports.SieveFingerprintDialog = SieveFingerprintDialog; - exports.SieveDeleteAccountDialog = SieveDeleteAccountDialog; - exports.SieveAuthorizationDialog = SieveAuthorizationDialog; - exports.SieveScriptBusyDialog = SieveScriptBusyDialog; - exports.SieveScriptSaveDialog = SieveScriptSaveDialog; - exports.SieveErrorDialog = SieveErrorDialog; - } - -})(this); diff --git a/src/common/managesieve.ui/dialogs/SieveDialogUI.mjs b/src/common/managesieve.ui/dialogs/SieveDialogUI.mjs new file mode 100644 index 00000000..8a8757c4 --- /dev/null +++ b/src/common/managesieve.ui/dialogs/SieveDialogUI.mjs @@ -0,0 +1,692 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +const DIALOG_CANCELED = 0; +const DIALOG_ACCEPTED = 1; +const DIALOG_DISCARDED = 2; + +/* global bootstrap */ +import { SieveTemplate } from "./../utils/SieveTemplate.mjs"; +import { SieveUniqueId } from "./../utils/SieveUniqueId.mjs"; + +/** + * Displays a simple dialog with an action button. + */ +class SieveDialog { + + /** + * The path to the html template which should be used for this dialog + * @abstract + * + * @returns {string} + * the path to the html template. + */ + getTemplate() { + throw new Error("Implement getTemplate"); + } + + /** + * Get the current dialogs root element + * + * @returns {HTMLElement} + * the dialogs root element. + */ + getDialog() { + return document.querySelector(`#${this.id}`); + } + + /** + * Called when the dialog is initialized. + * It is used to populate it with html elements. + * + * You can use getDialog to retrieve the dialogs root element. + * E.g. when adding new elements. + * + */ + onInit() { + throw new Error("Implement on Init"); + } + + /** + * Called when the user clicks the accept button. + * Which is any button marked with the "sieve-dialog-resolve" class. + * + * The result should be typically true for simple dialogs or + * with more complex dialogs just return the desired value. + * + * @returns {object} + * returns true or the accept value + * + */ + onAccept() { + return true; + } + + /** + * Called when the user clicks the cancel button. + * Which is any button marked with the "sieve-dialog-reject" class. + * + * The result should be false for simple dialogs or a default value for + * more complex dialogs. In case there is no default value, the best strategy + * is to throw an exception. + * + * @returns {object} + * returns false or the reject result. + */ + onCancel() { + return false; + } + + /** + * Called when the dialog is shown. + * This can be used e.g. to move the focus to the desired text box. + */ + onShown() { + } + + /** + * Generates an if made of alphanumerical characters and dashes. + * @returns {string} + * a string with an html compatible unique id + */ + generateId() { + return (new SieveUniqueId()).generate(); + } + + /** + * Removes the dialog window from the UI. + */ + destroy() { + const elm = this.getDialog(); + elm.remove(); + } + + /** + * Shows the dialog. + * + * @returns {object} + * the value returned from the dialog. + */ + async show() { + + this.id = this.generateId(); + + const dialog = await (new SieveTemplate()).load(this.getTemplate()); + dialog.id = this.id; + document.querySelector("#ctx").append(dialog); + + this.onInit(); + + return await new Promise((resolve, reject) => { + + const modal = new bootstrap.Modal(this.getDialog()); + + const buttons = this.getDialog() + .querySelectorAll(".sieve-dialog-resolve"); + + for (const button of buttons) { + button.addEventListener("click", async () => { + try { + resolve(await this.onAccept(event.target)); + } catch (ex) { + reject(ex); + } + + modal.hide(); + }); + } + + + modal.show(); + + this.getDialog().addEventListener('hidden.bs.modal', async () => { + + this.destroy(); + + try { + resolve(await this.onCancel()); + } catch (ex) { + reject(ex); + } + }); + + this.getDialog().addEventListener('shown.bs.modal', () => { + this.onShown(); + }); + }); + } +} + +/** + * Prompts if the given account shall be deleted. + */ +class SieveDeleteAccountDialog extends SieveDialog { + + /** + * Creates a new dialog instance + * + * @param {string} displayName + * the accounts display name + */ + constructor(displayName) { + super(); + this.displayName = displayName; + } + + /** + * @inheritdoc + */ + getTemplate() { + return "./dialogs/dialog.account.delete.html"; + } + + /** + * @inheritdoc + */ + onInit() { + this.getDialog() + .querySelector("#sieve-dialog-account-remove-name") + .textContent = this.displayName; + } +} + +/** + * Shows the dialog asking for the server's fingerprint. + */ +class SieveFingerprintDialog extends SieveDialog { + + /** + * Creates a new instance + * + * @param {object} secInfo + * the security info object with details about the validation error. + */ + constructor(secInfo) { + super(); + this.fingerprint = secInfo.fingerprint; + this.fingerprint256 = secInfo.fingerprint256; + this.error = secInfo.message; + } + + /** + * @inheritdoc + */ + getTemplate() { + return "./dialogs/dialog.account.cert.html"; + } + + /** + * @inheritdoc + */ + onInit() { + this.getDialog() + .querySelector(".sieve-dialog-fingerprint") + .textContent = this.fingerprint; + + if ((typeof (this.fingerprint256) !== "undefined") && (this.fingerprint256 !== null)) { + this.getDialog() + .querySelector(".sieve-dialog-fingerprint256") + .textContent = this.fingerprint256; + } + + this.getDialog() + .querySelector(".sieve-dialog-certerror") + .textContent = this.error; + } +} + +/** + * Aks if the given script should be deleted or not. + */ +class SieveDeleteScriptDialog extends SieveDialog { + + /** + * Create a new delete script dialog instance. + * + * @param {string} name + * the script name as string + */ + constructor(name) { + super(); + this.name = name; + } + + /** + * @inheritdoc + */ + getTemplate() { + return "./dialogs/dialog.script.delete.html"; + } + + /** + * @inheritdoc + */ + onInit() { + this.getDialog() + .querySelector('.sieve-delete-dialog-name') + .textContent = this.name; + } +} + +/** + * Asks for a name, which should be used to create the new script. + */ +class SieveCreateScriptDialog extends SieveDialog { + + /** + * @inheritdoc + */ + getTemplate() { + return "./dialogs/dialog.script.create.html"; + } + + /** + * @inheritdoc + */ + onInit() { + this.getDialog().querySelector('.sieve-create-dialog-name').addEventListener("keypress", (e) => { + if (e.key === "Enter") { + const event = document.createEvent('HTMLEvents'); + event.initEvent('click', true, false); + this.getDialog().querySelector(".sieve-dialog-resolve").dispatchEvent(event); + } + }); + } + + /** + * @inheritdoc + */ + onShown() { + this.getDialog().querySelector('.sieve-create-dialog-name').focus(); + } + + /** + * @inheritdoc + */ + onAccept() { + return this.getDialog().querySelector('.sieve-create-dialog-name').value; + } + + /** + * @inheritdoc + */ + onCancel() { + return ""; + } +} + +/** + * Asks for a scripts new name + */ +class SieveRenameScriptDialog extends SieveDialog { + + /** + * Create a rename script dialog instance. + * @param {string} name + * the scripts old name + */ + constructor(name) { + super(); + this.name = name; + } + + /** + * @inheritdoc + */ + getTemplate() { + return "./dialogs/dialog.script.rename.html"; + } + + /** + * @inheritdoc + */ + onInit() { + this.getDialog().querySelector('.sieve-rename-dialog-newname').addEventListener("keypress", (e) => { + if (e.key === "Enter") { + const event = document.createEvent('HTMLEvents'); + event.initEvent('click', true, false); + this.getDialog().querySelector(".sieve-dialog-resolve").dispatchEvent(event); + } + }); + + this.getDialog() + .querySelector(".sieve-rename-dialog-newname").value = this.name; + } + + /** + * @inheritdoc + */ + onShown() { + this.getDialog().querySelector('.sieve-rename-dialog-newname').focus(); + } + + /** + * @inheritdoc + */ + onAccept() { + return this.getDialog() + .querySelector(".sieve-rename-dialog-newname").value; + } + + /** + * @inheritdoc + */ + onCancel() { + return this.name; + } +} + +/** + * Asks for a password. + * The show method returns null or the authorization. + */ +class SievePasswordDialog extends SieveDialog { + + /** + * Creates a password dialog. + * + * @param {string} username + * the username for which the password is requested + * @param {string} displayName + * the accounts display name. + * @param {{ remember : boolean }} [options] + * extended additional options. + * In case "remember" is set to true a switch will be rendered which allows + * the user to select if the password should be stored. + */ + constructor(username, displayName, options) { + super(); + this.username = username; + this.displayName = displayName; + + if (typeof (options) === "undefined" || options === null) + options = {}; + + this.options = options; + } + + /** + * @inheritdoc + */ + getTemplate() { + return "./dialogs/dialog.account.password.html"; + } + + /** + * @inheritdoc + */ + onInit() { + const dialog = this.getDialog(); + + if (!this.options.remember) + dialog.querySelector(".sieve-password-remember").style.display = "none"; + + dialog.querySelector(".sieve-username").textContent = this.username; + dialog.querySelector(".sieve-displayname").textContent = this.displayName; + + this.getDialog().querySelector('.sieve-password').addEventListener("keypress", (e) => { + if (e.key === "Enter") { + const event = document.createEvent('HTMLEvents'); + event.initEvent('click', true, false); + this.getDialog().querySelector(".sieve-dialog-resolve").dispatchEvent(event); + } + }); + } + + /** + * @inheritdoc + */ + onShown() { + this.getDialog().querySelector('.sieve-password').focus(); + } + + /** + * @inheritdoc + */ + onAccept() { + return { + "username": this.username, + "password": this.getDialog().querySelector(".sieve-password").value, + "remember": document.querySelector("#sieve-password-remember").checked + }; + } + + /** + * @inheritdoc + */ + onCancel() { + return {}; + } +} + +/** + * Asks for a authorization. + * The show method returns null or the authorization. + */ +class SieveAuthorizationDialog extends SieveDialog { + + /** + * Creates a authorization request dialog. + * + * @param {string} displayName + * the account's display name. + */ + constructor(displayName) { + super(); + this.displayName = displayName; + } + + /** + * @inheritdoc + */ + getTemplate() { + return "./dialogs/dialog.account.authorization.html"; + } + + /** + * @inheritdoc + */ + onInit() { + const dialog = this.getDialog(); + + dialog.querySelector(".sieve-displayname").textContent = this.displayName; + + dialog.querySelector('.sieve-authorization').addEventListener("keypress", (e) => { + if (e.key === "Enter") { + const event = document.createEvent('HTMLEvents'); + event.initEvent('click', true, false); + this.getDialog().querySelector(".sieve-dialog-resolve").dispatchEvent(event); + } + }); + } + + /** + * @inheritdoc + */ + onShown() { + this.getDialog().querySelector('.sieve-authorization').focus(); + } + + /** + * @inheritdoc + */ + onAccept() { + return this.getDialog().querySelector(".sieve-authorization").value; + } + + /** + * @inheritdoc + */ + onCancel() { + return null; + } +} + +/** + * An info dialog indicating the current script is busy. + */ +class SieveScriptBusyDialog extends SieveDialog { + + /** + * Create a new script busy dialog. + * @param {string} name + * the scripts name. + */ + constructor(name) { + super(); + this.name = name; + } + + /** + * @inheritdoc + */ + getTemplate() { + return "./dialogs/dialog.script.busy.html"; + } + + /** + * @inheritdoc + */ + onInit() { + this.getDialog() + .querySelector(".sieve-busy-dialog-scriptname").textContent = this.name; + } +} + +// TODO should be extracted an stored next to the editor +/** + * Asks is a changed script should be saved + */ +class SieveScriptSaveDialog extends SieveDialog { + + /** + * Create a rename script dialog instance. + * @param {string} name + * the scripts old name + */ + constructor(name) { + super(); + this.name = name; + } + + /** + * @inheritdoc + */ + getTemplate() { + return "./dialogs/dialog.script.save.html"; + } + + /** + * @inheritdoc + */ + onInit() { + this.getDialog() + .querySelector(".sieve-save-dialog-scriptname").textContent = this.name; + } + + /** + * @inheritdoc + */ + onAccept(element) { + if (element.classList.contains("sieve-save-dialog-discard")) + return DIALOG_DISCARDED; + + if (element.classList.contains("sieve-save-dialog-save")) + return DIALOG_ACCEPTED; + + throw new Error("Unknown button pressed"); + } + + /** + * Checks if the dialogs return value was a discard + * @param {int} value + * the value to be checked. + * @returns {boolean} + * true in case the dialog was discarded. + */ + static isDiscarded(value) { + return value === DIALOG_DISCARDED; + } + + /** + * Checks if the dialogs return value was a cancel + * @param {int} value + * the value to be checked. + * @returns {boolean} + * true in case the dialog was canceled + */ + static isCanceled(value) { + return value === DIALOG_CANCELED; + } + + /** + * Checks if the dialogs return value was an accept + * @param {int} value + * the value to be checked. + * @returns {boolean} + * true in case the dialog was accepted. + */ + static isAccepted(value) { + return value === DIALOG_ACCEPTED; + } + + /** + * @inheritdoc + */ + onCancel() { + return DIALOG_CANCELED; + } +} + +/** + * An info dialog indicating the current script is busy. + */ +class SieveErrorDialog extends SieveDialog { + + /** + * Create a new script busy dialog. + * @param {string} description + * the scripts name. + */ + constructor(description) { + super(); + this.description = description; + } + + /** + * @inheritdoc + */ + getTemplate() { + return "./dialogs/dialog.error.html"; + } + + /** + * @inheritdoc + */ + onInit() { + this.getDialog() + .querySelector(".sieve-error-dialog-description") + .textContent = this.description; + } +} + +export { + SievePasswordDialog, + SieveRenameScriptDialog, + SieveCreateScriptDialog, + SieveDeleteScriptDialog, + SieveFingerprintDialog, + SieveDeleteAccountDialog, + SieveAuthorizationDialog, + SieveScriptBusyDialog, + SieveScriptSaveDialog, + SieveErrorDialog +}; diff --git a/src/common/managesieve.ui/dialogs/dialog.account.authorization.tpl b/src/common/managesieve.ui/dialogs/dialog.account.authorization.html index e0842545..278ec304 100644 --- a/src/common/managesieve.ui/dialogs/dialog.account.authorization.tpl +++ b/src/common/managesieve.ui/dialogs/dialog.account.authorization.html @@ -4,7 +4,7 @@ <div class="modal-content"> <div class="modal-header"> <h5 data-i18n="authorization.title" class="modal-title"></h5> - <button type="button" class="btn-close" data-dismiss="modal" aria-label="Close"></button> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> <div class="mb-3 row"> diff --git a/src/common/managesieve.ui/dialogs/dialog.account.cert.tpl b/src/common/managesieve.ui/dialogs/dialog.account.cert.html index a4a8c464..bbe709cc 100644 --- a/src/common/managesieve.ui/dialogs/dialog.account.cert.tpl +++ b/src/common/managesieve.ui/dialogs/dialog.account.cert.html @@ -1,24 +1,23 @@ <div class="modal fade" tabindex="-1" role="dialog" aria-hidden="true"> - <div class="modal-dialog" role="document"> + <div class="modal-dialog modal-lg" role="document"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" data-i18n="cert.title"></h5> - <button type="button" class="btn-close" data-dismiss="modal" aria-label="Close"></button> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> - <p data-i18n="cert.description1"></p> - <div class="alert alert-danger" role="alert"> - <p data-i18n="cert.description2"></p> + <b><p data-i18n="cert.description1"></p></b> - <p data-i18n="cert.error"></p> - <p class="sieve-dialog-certerror"></p> + <div class="alert alert-danger" role="alert"> + <b><p class="sieve-dialog-certerror"></p></b> - <p data-i18n="cert.fingerprint"></p> - <p class="sieve-dialog-fingerprint"></p> + <p class="sieve-dialog-fingerprint text-break"></p> + <p class="sieve-dialog-fingerprint256 text-break"></p> </div> <p data-i18n="cert.warning"></p> + <p data-i18n="cert.warning2"></p> </div> <div class="modal-footer"> <button data-i18n="cert.accept" type="button" class="btn btn-danger sieve-dialog-resolve"></button> diff --git a/src/common/managesieve.ui/dialogs/dialog.account.delete.tpl b/src/common/managesieve.ui/dialogs/dialog.account.delete.html index 1b2b6cb9..7a5aab29 100644 --- a/src/common/managesieve.ui/dialogs/dialog.account.delete.tpl +++ b/src/common/managesieve.ui/dialogs/dialog.account.delete.html @@ -3,7 +3,7 @@ <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" data-i18n="account.delete.title"></h5> - <button type="button" class="btn-close" data-dismiss="modal" aria-label="Close"></button> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> <div> diff --git a/src/common/managesieve.ui/dialogs/dialog.account.password.tpl b/src/common/managesieve.ui/dialogs/dialog.account.password.html index 6b70892a..fab5aeac 100644 --- a/src/common/managesieve.ui/dialogs/dialog.account.password.tpl +++ b/src/common/managesieve.ui/dialogs/dialog.account.password.html @@ -4,7 +4,7 @@ <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" data-i18n="password.dialog.title"></h5> - <button type="button" class="btn-close" data-dismiss="modal" aria-label="Close"></button> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> <div class="mb-3 row"> @@ -26,7 +26,7 @@ </div> <div class="mb-3 row sieve-password-remember"> <label class="col-sm-3 col-form-label"></label> - <div class="col-sm-8 ml-3 custom-control custom-switch"> + <div class="col-sm-8 ms-3 custom-control custom-switch"> <input type="checkbox" class="form-check-input" id="sieve-password-remember"> <label class="form-check-label" for="sieve-password-remember" data-i18n="password.dialog.remember"></label> </div> diff --git a/src/common/managesieve.ui/dialogs/dialog.error.tpl b/src/common/managesieve.ui/dialogs/dialog.error.html index 9580d0aa..8855df68 100644 --- a/src/common/managesieve.ui/dialogs/dialog.error.tpl +++ b/src/common/managesieve.ui/dialogs/dialog.error.html @@ -3,7 +3,7 @@ <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" data-i18n="account.error.title"></h5> - <button type="button" class="btn-close" data-dismiss="modal" aria-label="Close"></button> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> <div data-i18n="account.error.description"></div> diff --git a/src/common/managesieve.ui/dialogs/dialog.script.busy.tpl b/src/common/managesieve.ui/dialogs/dialog.script.busy.html index 1e0049e8..28d74bc6 100644 --- a/src/common/managesieve.ui/dialogs/dialog.script.busy.tpl +++ b/src/common/managesieve.ui/dialogs/dialog.script.busy.html @@ -4,7 +4,7 @@ <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" data-i18n="script.busy.title"></h5> - <button type="button" class="btn-close" data-dismiss="modal" aria-label="Close"></button> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> <div> diff --git a/src/common/managesieve.ui/dialogs/dialog.script.create.tpl b/src/common/managesieve.ui/dialogs/dialog.script.create.html index 8478d062..ec291dd0 100644 --- a/src/common/managesieve.ui/dialogs/dialog.script.create.tpl +++ b/src/common/managesieve.ui/dialogs/dialog.script.create.html @@ -3,7 +3,7 @@ <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" data-i18n="script.create.title"></h5> - <button type="button" class="btn-close" data-dismiss="modal" aria-label="Close"></button> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> <div data-i18n="script.create.description"></div> diff --git a/src/common/managesieve.ui/dialogs/dialog.script.delete.tpl b/src/common/managesieve.ui/dialogs/dialog.script.delete.html index db678a7e..f83b680f 100644 --- a/src/common/managesieve.ui/dialogs/dialog.script.delete.tpl +++ b/src/common/managesieve.ui/dialogs/dialog.script.delete.html @@ -4,7 +4,7 @@ <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" data-i18n="script.delete.title" id="deleteScriptLabel"></h5> - <button type="button" class="btn-close" data-dismiss="modal" aria-label="Close"></button> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> <div> diff --git a/src/common/managesieve.ui/dialogs/dialog.script.rename.tpl b/src/common/managesieve.ui/dialogs/dialog.script.rename.html index e4a0f8ea..8a2ff06c 100644 --- a/src/common/managesieve.ui/dialogs/dialog.script.rename.tpl +++ b/src/common/managesieve.ui/dialogs/dialog.script.rename.html @@ -4,7 +4,7 @@ <div class="modal-content"> <div class="modal-header"> <h5 data-i18n="script.rename.title" class="modal-title"></h5> - <button type="button" class="btn-close" data-dismiss="modal" aria-label="Close"></button> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> <div data-i18n="script.rename.description"></div> diff --git a/src/common/managesieve.ui/dialogs/dialog.script.save.tpl b/src/common/managesieve.ui/dialogs/dialog.script.save.html index 1a7ede49..a1b47741 100644 --- a/src/common/managesieve.ui/dialogs/dialog.script.save.tpl +++ b/src/common/managesieve.ui/dialogs/dialog.script.save.html @@ -4,7 +4,7 @@ <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" data-i18n="editor.save.title"></h5> - <button type="button" class="btn-close" data-dismiss="modal" aria-label="Close"></button> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> <div> diff --git a/src/common/managesieve.ui/editor.html b/src/common/managesieve.ui/editor.html index 89e9d3d0..5e05ff87 100644 --- a/src/common/managesieve.ui/editor.html +++ b/src/common/managesieve.ui/editor.html @@ -72,25 +72,7 @@ <!-- Placed at the end of the document so the pages load faster --> <script src="./../../libs/bootstrap/js/bootstrap.bundle.min.js"></script> - <script src="./../../libs/managesieve.ui/utils/SieveFakeRequire.js"></script> - - <script src="./../../libs/managesieve.ui/utils/SieveLogger.js"></script> - <script src="./../../libs/managesieve.ui/utils/SieveUniqueId.js"></script> - <script src="./../../libs/managesieve.ui/utils/SieveAbstractIpcClient.js"></script> - <script src="./../../libs/managesieve.ui/utils/SieveIpcClient.js"></script> - <script src="./../../libs/managesieve.ui/utils/SieveI18n.js"></script> - <script src="./../../libs/managesieve.ui/utils/SieveTemplate.js"></script> - - <script src="./../../libs/managesieve.ui/editor/SieveAbstractEditor.js"></script> - <script src="./../../libs/managesieve.ui/editor/SieveEditorController.js"></script> - <script src="./../../libs/managesieve.ui/editor/graphical/SieveGraphicalEditor.js"></script> - <script src="./../../libs/managesieve.ui/editor/text/SieveTextEditor.js"></script> - - <script src="./dialogs/SieveDialogUI.js"></script> - - <script src="./editor/SieveEditor.js"></script> - - <script src="editor.js"></script> + <script type="module" src="editor.mjs"></script> </body> diff --git a/src/common/managesieve.ui/editor.js b/src/common/managesieve.ui/editor.js deleted file mode 100644 index c2a7d87d..00000000 --- a/src/common/managesieve.ui/editor.js +++ /dev/null @@ -1,90 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(() => { - - "use strict"; - - /* global SieveEditorUI */ - /* global SieveIpcClient */ - /* global SieveLogger */ - /* global SieveI18n */ - /* global SieveScriptSaveDialog */ - - let editor = null; - - /** - * Called when the editor is about to be closed. - * Asks if the script should be saved or closing the window should be aborted. - * - * @param {string} name - * the script name - * @returns {boolean} - * true in case the editor can be close, otherwise false. - */ - async function onClose(name) { - - const result = await(new SieveScriptSaveDialog(name).show()); - - if (SieveScriptSaveDialog.isCanceled(result)) - return false; - - if (SieveScriptSaveDialog.isAccepted(result)) - return await editor.save(); - - return true; - } - - /** - * The main entry point. - * Called as soon as the DOM is ready. - */ - async function main() { - - SieveLogger.getInstance().level( - await SieveIpcClient.sendMessage("core", "settings-get-loglevel")); - - await (SieveI18n.getInstance()).load(); - - const url = new URL(window.location); - const script = url.searchParams.get("script"); - const account = url.searchParams.get("account"); - - // initialize the editor - editor = new SieveEditorUI(script, account, "code"); - - await editor.render(); - await editor.load(); - - SieveIpcClient.setRequestHandler("editor", "editor-close", - async (msg) => { return await onClose(msg.payload); }); - SieveIpcClient.setRequestHandler("editor", "editor-shown", () => { window.focus(); editor.focus(); }); - SieveIpcClient.setRequestHandler("editor", "editor-hasChanged", async () => { return await editor.hasChanged(); }); - - // TODO Send a ready signal... - } - - if (document.readyState !== 'loading') - main(); - else - document.addEventListener('DOMContentLoaded', () => { main(); }, {once: true}); - - /* - CodeMirror.on(window, "resize", function() { - document.body.getElementsByClassName("CodeMirror-fullscreen")[0] - .CodeMirror.getWrapperElement().style.height = winHeight() + "px"; - }); - */ - - // hlLine = editor.addLineClass(0, "background", "activeline"); - - // editor.on("change", function() { onChange(); }); -})(); diff --git a/src/common/managesieve.ui/editor.mjs b/src/common/managesieve.ui/editor.mjs new file mode 100644 index 00000000..059e13c6 --- /dev/null +++ b/src/common/managesieve.ui/editor.mjs @@ -0,0 +1,88 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +import { SieveIpcClient } from "./utils/SieveIpcClient.mjs"; +import { SieveLogger } from "./utils/SieveLogger.mjs"; +import { SieveI18n } from "./utils/SieveI18n.mjs"; + +import { SieveEditorUI } from "./editor/SieveEditor.mjs"; +import { SieveScriptSaveDialog } from "./dialogs/SieveDialogUI.mjs"; + + +let editor = null; + +/** + * Called when the editor is about to be closed. + * Asks if the script should be saved or closing the window should be aborted. + * + * @param {string} name + * the script name + * @returns {boolean} + * true in case the editor can be close, otherwise false. + */ +async function onClose(name) { + + const result = await (new SieveScriptSaveDialog(name).show()); + + if (SieveScriptSaveDialog.isCanceled(result)) + return false; + + if (SieveScriptSaveDialog.isAccepted(result)) + return await editor.save(); + + return true; +} + +/** + * The main entry point. + * Called as soon as the DOM is ready. + */ +async function main() { + + SieveLogger.getInstance().level( + await SieveIpcClient.sendMessage("core", "settings-get-loglevel")); + + await (SieveI18n.getInstance()).load(); + + const url = new URL(window.location); + const script = url.searchParams.get("script"); + const account = url.searchParams.get("account"); + + // initialize the editor + editor = new SieveEditorUI(script, account, "code"); + + await editor.render(); + await editor.load(); + + SieveIpcClient.setRequestHandler("editor", "editor-close", + async (msg) => { return await onClose(msg.payload); }); + SieveIpcClient.setRequestHandler("editor", "editor-shown", () => { window.focus(); editor.focus(); }); + SieveIpcClient.setRequestHandler("editor", "editor-hasChanged", async () => { return await editor.hasChanged(); }); + + // TODO Send a ready signal... +} + +if (document.readyState !== 'loading') + main(); +else + document.addEventListener('DOMContentLoaded', () => { main(); }, { once: true }); + +/* +CodeMirror.on(window, "resize", function() { + document.body.getElementsByClassName("CodeMirror-fullscreen")[0] + .CodeMirror.getWrapperElement().style.height = winHeight() + "px"; +}); +*/ + +// hlLine = editor.addLineClass(0, "background", "activeline"); + +// editor.on("change", function() { onChange(); }); + diff --git a/src/common/managesieve.ui/editor/SieveAbstractEditor.js b/src/common/managesieve.ui/editor/SieveAbstractEditor.js deleted file mode 100644 index 1db052ec..00000000 --- a/src/common/managesieve.ui/editor/SieveAbstractEditor.js +++ /dev/null @@ -1,134 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - const HEX = 16; - const HEX_LENGTH = 2; - - /** - * An abstract and generic sieve editor interface . - */ - class SieveAbstractEditorUI { - - /** - * Creates a new text editor UI. - * - * @param {SieveEditorController} controller - * the controller which is assigned to this editor. - */ - constructor(controller) { - this.controller = controller; - } - - /** - * The controller implements an editors external interfaces and actions. - * It is typically shared between editors. - * - * @returns {SieveEditorController} - * the controller which assigned to this editor. - */ - getController() { - return this.controller; - } - - /** - * Renders the current editor. - * @abstract - */ - async render() { - } - - /** - * Sets the editor's content. - * @abstract - * - * @param {string} script - * the script - */ - // eslint-disable-next-line require-await - async setScript(script) { - throw new Error(`Implement setScript(${script})`); - } - - /** - * Gets the editor's content. - * @abstract - * - * @returns {string} - * the current script as string. - */ - getScript() { - throw new Error("Implement getScript()"); - } - - /** - * Saves the script. - */ - async save() { - await this.controller.save(); - } - - /** - * Focuses the editor's text area - */ - focus() { - } - - /** - * Clears the editors - */ - clearHistory() { - } - - /** - * Calculate the checksum for the current context script. - * - * @returns {string} - * the content scripts sha256 checksum. - */ - async getChecksum() { - - const digest = await crypto.subtle.digest('SHA-256', - new TextEncoder().encode(await this.getScript())); - - return Array.from(new Uint8Array(digest)).map( - (b) => { return b.toString(HEX).padStart(HEX_LENGTH, '0'); }).join(''); - } - - /** - * Loads the editors settings. - */ - async loadSettings() { - } - - /** - * Resets the editor to default settings - */ - async loadDefaultSettings() { - } - - /** - * Save the current settings as default. - */ - async saveDefaultSettings() { - } - - } - - if (typeof (module) !== "undefined" && module && module.exports) - module.exports.SieveAbstractEditorUI = SieveAbstractEditorUI; - else - exports.SieveAbstractEditorUI = SieveAbstractEditorUI; - -})(this); diff --git a/src/common/managesieve.ui/editor/SieveAbstractEditor.mjs b/src/common/managesieve.ui/editor/SieveAbstractEditor.mjs new file mode 100644 index 00000000..a8da509a --- /dev/null +++ b/src/common/managesieve.ui/editor/SieveAbstractEditor.mjs @@ -0,0 +1,124 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +const HEX = 16; +const HEX_LENGTH = 2; + +/** + * An abstract and generic sieve editor interface . + */ +class SieveAbstractEditorUI { + + /** + * Creates a new text editor UI. + * + * @param {SieveEditorController} controller + * the controller which is assigned to this editor. + */ + constructor(controller) { + this.controller = controller; + } + + /** + * The controller implements an editors external interfaces and actions. + * It is typically shared between editors. + * + * @returns {SieveEditorController} + * the controller which assigned to this editor. + */ + getController() { + return this.controller; + } + + /** + * Renders the current editor. + * @abstract + */ + async render() { + } + + /** + * Sets the editor's content. + * @abstract + * + * @param {string} script + * the script + */ + async setScript(script) { + throw new Error(`Implement setScript(${script})`); + } + + /** + * Gets the editor's content. + * @abstract + * + * @returns {string} + * the current script as string. + */ + getScript() { + throw new Error("Implement getScript()"); + } + + /** + * Saves the script. + */ + async save() { + await this.controller.save(); + } + + /** + * Focuses the editor's text area + */ + focus() { + } + + /** + * Clears the editors + */ + clearHistory() { + } + + /** + * Calculate the checksum for the current context script. + * + * @returns {string} + * the content scripts sha256 checksum. + */ + async getChecksum() { + + const digest = await crypto.subtle.digest('SHA-256', + (new TextEncoder()).encode(await this.getScript())); + + return [...new Uint8Array(digest)].map( + (b) => { return b.toString(HEX).padStart(HEX_LENGTH, '0'); }).join(''); + } + + /** + * Loads the editors settings. + */ + async loadSettings() { + } + + /** + * Resets the editor to default settings + */ + async loadDefaultSettings() { + } + + /** + * Save the current settings as default. + */ + async saveDefaultSettings() { + } + +} + +export { SieveAbstractEditorUI }; diff --git a/src/common/managesieve.ui/editor/SieveEditor.js b/src/common/managesieve.ui/editor/SieveEditor.js deleted file mode 100644 index fbf4831e..00000000 --- a/src/common/managesieve.ui/editor/SieveEditor.js +++ /dev/null @@ -1,467 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - /* global bootstrap */ - /* global SieveEditorController */ - /* global SieveTextEditorUI */ - /* global SieveGraphicalEditorUI */ - /* global SieveTemplate */ - - const EDITOR_OFFSET_PX = 40; - /** - * Implements a editor UI which contains a graphical as well as a text editor. - */ - class SieveEditorUI extends SieveEditorController { - - /** - * @inheritdoc - */ - constructor(name, account) { - super(name, account); - - this.textEditor = new SieveTextEditorUI(this); - this.graphicalEditor = new SieveGraphicalEditorUI(this); - this.checksum = ""; - } - - /** - * Resizes the widget editors iframe to fill all of the available screen. - */ - resize() { - const topOffset = document - .querySelector("#sieve-widget-editor") - .getBoundingClientRect() - .top + document.body.scrollTop; - - const screen = document.documentElement.clientHeight; - - // the visible screen size minus the top and bottom offset - const size = screen - topOffset - EDITOR_OFFSET_PX; - - document - .querySelector("#sieve-widget-editor") - .style.height = `${size}px`; - } - - /** - * Moves the input focus to the currently active editor. - */ - focus() { - this.getCurrentEditor().focus(); - } - - /** - * Renders the editor to screen. - * - * @returns {SieveEditorUI} - * a self reference. - */ - async render() { - - document.querySelector("#sieve-editor").appendChild( - await (new SieveTemplate()).load("./editor/editor.tpl")); - - await this.getTextEditor().render(); - await this.getGraphicalEditor().render(); - - await this.loadSettings(); - - document - .querySelector("#sieve-editor-settings .sieve-editor-settings-show") - .addEventListener("click", () => { - (new bootstrap.Tab(document.querySelector("#sieve-tab-settings"))).show(); - }); - - document - .querySelector("#sieve-editor-settings .sieve-editor-import") - .addEventListener("click", () => { - this.importScript(); - }); - - document - .querySelector("#sieve-editor-settings .sieve-editor-export") - .addEventListener("click", () => { - this.exportScript(); - }); - - document - .querySelector("#sieve-editor-save") - .addEventListener("click", async () => { - document - .querySelector("#sieve-editor-saving").classList.remove("d-none"); - - await this.save(); - - document - .querySelector("#sieve-editor-saving").classList.add("d-none"); - }); - - document - .querySelector('.nav-item > a[href="#sieve-widget-editor"]') - .addEventListener('show.bs.tab', async (e) => { - - if (!this.isTextEditor()) - return; - - e.preventDefault(); - - if (await this.switchToGraphicalEditor()) { - (new bootstrap.Tab(document.querySelector('.nav-item > a[href="#sieve-widget-editor"]'))).show(); - } - }); - - document - .querySelector('.nav-item > a[href="#sieve-widget-editor"]') - .addEventListener('shown.bs.tab', () => { - this.resize(); - }); - - window.addEventListener("resize", () => { - this.resize(); - }); - - document - .querySelector('.nav-item > a[href="#sieve-plaintext-editor"]') - .addEventListener('shown.bs.tab', () => { this.switchToTextEditor(); }); - - document - .querySelector('.nav-item > a[href="#sieve-content-settings"]') - .addEventListener('shown.bs.tab', () => { this.switchToSettings(); }); - - return this; - } - - /** - * Hides/Dismisses any error messages. - */ - hideErrorMessage() { - const elm = document.querySelector("#sieve-editor-error"); - if (elm !== null) - elm.parentNode.removeChild(elm); - } - - /** - * Shows an error message. - * - * @param {string} message - * the error message to show. - */ - async showErrorMessage(message) { - - const content = await (new SieveTemplate()).load("./editor/editor.error.save.html"); - - content.querySelector(".sieve-editor-error-msg").textContent = message; - - this.hideErrorMessage(); - - document.querySelector("#sieve-editor-errors").appendChild(content); - - // eslint-disable-next-line no-new - new bootstrap.Alert(content); - - this.resize(); - } - - /** - * Checks if the current editor has unsaved changes - * - * @returns {boolean} - * true in case the editor contains unsaved changes. - */ - async hasChanged() { - return (await this.getCurrentEditor().getChecksum() !== this.checksum); - } - - /** - * Loads the sieve script into the editor. - * All undo history will be flushed. - * - * @returns {boolean} - * true in case the script could be loaded otherwise false. - */ - async load() { - - const editor = this.getCurrentEditor(); - - await editor.setScript(await this.loadScript()); - - this.checksum = await editor.getChecksum(); - - editor.clearHistory(); - editor.focus(); - - return true; - } - - /** - * Saves the sieve script currently loaded into the editor. - * - * @returns {boolean} - * true in case the script could be saved otherwise false. - */ - async save() { - - if (!await this.hasChanged()) - return this; - - try { - - const editor = this.getCurrentEditor(); - - await this.saveScript( - await editor.getScript()); - - this.checksum = await editor.getChecksum(); - - this.hideErrorMessage(); - } catch (ex) { - this.showErrorMessage(ex.toString()); - return false; - } - - return true; - } - - /** - * Checks the script on the server for syntax errors. - * - * @param {string} [script] - * the script to check if omitted the current editors - * script will be used. - * - * @returns {undefined|string} - * in case of an script error, the error message - * otherwise null - */ - async checkScript(script) { - if (script === undefined || script === null) - script = await this.getCurrentEditor().getScript(); - - return await super.checkScript(script); - } - - /** - * Imports a sieve script from a file - */ - async importScript() { - - const script = await super.importScript(); - - if (typeof (script) === "undefined") - return; - - await this.getCurrentEditor().setScript(script); - - // The dialog stole our focus... - this.getCurrentEditor().focus(); - } - - /** - * Exports the script to a file - */ - async exportScript() { - await super.exportScript(await this.getCurrentEditor().getScript()); - - // The dialog stole our focus... - this.getCurrentEditor().focus(); - } - - /** - * Switches to the text editor. - * It transfers the script from the graphical to the text editor. - * - * @returns {boolean} - * true in case the editor was changed otherwise false. - */ - async switchToTextEditor() { - - document.querySelector("#sieve-editor-save").classList.remove("d-none"); - document.querySelector("#sieve-editor-toolbar").classList.remove("d-none"); - document.querySelector("#sieve-plaintext-editor-toolbar").classList.remove("d-none"); - - if (this.isTextEditor()) { - this.getTextEditor().focus(); - return true; - } - - // We keep the history intact so that undo works. - // The set script would be just like an edit.. - await this.getTextEditor().setScript( - await this.getGraphicalEditor().getScript()); - - this.isTextEditor(true); - - this.getTextEditor().focus(); - - return true; - } - - /** - * Switches the graphical editor. - * It transfers the script from the text editor to the graphical. - * It is only possible to switch to the graphical editor, - * when the script is free of syntax errors. - * - * @returns {boolean} - * true in case the editor was changed otherwise false. - */ - async switchToGraphicalEditor() { - - document.querySelector("#sieve-editor-save").classList.remove("d-none"); - document.querySelector("#sieve-editor-toolbar").classList.add("d-none"); - document.querySelector("#sieve-plaintext-editor-toolbar").classList.add("d-none"); - - if (!this.isTextEditor()) - return true; - - try { - await this.getGraphicalEditor().setScript( - await this.getTextEditor().getScript()); - } catch (ex) { - await this.switchToTextEditor(); - this.showErrorMessage(`Switching to Graphical editor failed ${ex}`); - return false; - } - - this.isTextEditor(false); - return true; - } - - /** - * Switches to the settings tab. - */ - switchToSettings() { - document.querySelector("#sieve-editor-toolbar").classList.add("d-none"); - document.querySelector("#sieve-editor-save").classList.add("d-none"); - } - - /** - * Gets the currently active editor type and optionally - * also sets the editor type. - * - * @param {boolean} [value] - * optional. If set to true the text editor will be enabled. - * Setting it to false will switch to the graphical editor. - * - * @returns {boolean} - * true in case the text editor is the current editor. - * Otherwise false. - */ - isTextEditor(value) { - - const elm = document.querySelector('.nav-item > a[href="#sieve-widget-editor"]'); - - if (value === true || value === false) - elm.dataset.currentEditor = (!value).toString(); - - return !(elm.dataset.currentEditor === "true"); - } - - /** - * Returns a reference to the currently active editor. - * Which is either the plain text editor or the rich text editor. - * - * @returns {SieveAbstractEditorUI} the currently active editor. - */ - getCurrentEditor() { - if (this.isTextEditor()) - return this.getTextEditor(); - - return this.getGraphicalEditor(); - } - - /** - * Returns the plain text editor. - * Keep in mind the editor might be hidden. - * - * @returns {SieveTextEditorUI} - * the plain text editor - */ - getTextEditor() { - return this.textEditor; - } - - /** - * Returns the graphical editor. - * Keep in mind the editor might be hidden. - * - * @returns {SieveGraphicalEditorUI} - * the graphical editor - */ - getGraphicalEditor() { - return this.graphicalEditor; - } - - /** - * Renders the current settings. - */ - async renderSettings() { - - const parent = document.querySelector("#sieve-content-settings"); - while (parent.firstChild) - parent.removeChild(parent.firstChild); - - await this.getTextEditor().renderSettings(); - // this.getGraphicalEditor().renderSettings(); - - parent.appendChild( - await (new SieveTemplate()).load("./editor/editor.settings.defaults.tpl")); - - document.querySelector("#editor-settings-save-defaults") - .addEventListener("click", async () => { - await this.saveDefaultSettings(); - }); - - document.querySelector("#editor-settings-load-defaults") - .addEventListener("click", async () => { - await this.loadDefaultSettings(); - }); - } - - /** - * @inheritdoc - */ - async loadSettings() { - await this.getTextEditor().loadSettings(); - await this.getGraphicalEditor().loadSettings(); - - await this.renderSettings(); - } - - /** - * @inheritdoc - */ - async loadDefaultSettings() { - await this.getTextEditor().loadDefaultSettings(); - await this.getGraphicalEditor().loadDefaultSettings(); - - await this.renderSettings(); - } - - /** - * @inheritdoc - */ - async saveDefaultSettings() { - await this.getTextEditor().saveDefaultSettings(); - await this.getGraphicalEditor().saveDefaultSettings(); - } - } - - if (typeof (module) !== "undefined" && module && module.exports) - module.exports.SieveEditorUI = SieveEditorUI; - else - exports.SieveEditorUI = SieveEditorUI; - -})(this); diff --git a/src/common/managesieve.ui/editor/SieveEditor.mjs b/src/common/managesieve.ui/editor/SieveEditor.mjs new file mode 100644 index 00000000..36661df2 --- /dev/null +++ b/src/common/managesieve.ui/editor/SieveEditor.mjs @@ -0,0 +1,459 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +/* global bootstrap */ + +import { SieveEditorController } from "./SieveEditorController.mjs"; +import { SieveTextEditorUI } from "./text/SieveTextEditor.mjs"; +import { SieveGraphicalEditorUI } from "./graphical/SieveGraphicalEditor.mjs"; +import { SieveTemplate } from "./../utils/SieveTemplate.mjs"; + +const EDITOR_OFFSET_PX = 40; +/** + * Implements a editor UI which contains a graphical as well as a text editor. + */ +class SieveEditorUI extends SieveEditorController { + + /** + * @inheritdoc + */ + constructor(name, account) { + super(name, account); + + this.textEditor = new SieveTextEditorUI(this); + this.graphicalEditor = new SieveGraphicalEditorUI(this); + this.checksum = ""; + } + + /** + * Resizes the widget editors iframe to fill all of the available screen. + */ + resize() { + const topOffset = document + .querySelector("#sieve-widget-editor") + .getBoundingClientRect() + .top + document.body.scrollTop; + + const screen = document.documentElement.clientHeight; + + // the visible screen size minus the top and bottom offset + const size = screen - topOffset - EDITOR_OFFSET_PX; + + document + .querySelector("#sieve-widget-editor") + .style.height = `${size}px`; + } + + /** + * Moves the input focus to the currently active editor. + */ + focus() { + this.getCurrentEditor().focus(); + } + + /** + * Renders the editor to screen. + * + * @returns {SieveEditorUI} + * a self reference. + */ + async render() { + + document.querySelector("#sieve-editor").append( + await (new SieveTemplate()).load("./editor/editor.html")); + + await this.getTextEditor().render(); + await this.getGraphicalEditor().render(); + + await this.loadSettings(); + + document + .querySelector("#sieve-editor-settings .sieve-editor-settings-show") + .addEventListener("click", () => { + (new bootstrap.Tab("#sieve-tab-settings")).show(); + }); + + document + .querySelector("#sieve-editor-settings .sieve-editor-import") + .addEventListener("click", () => { + this.importScript(); + }); + + document + .querySelector("#sieve-editor-settings .sieve-editor-export") + .addEventListener("click", () => { + this.exportScript(); + }); + + document + .querySelector("#sieve-editor-save") + .addEventListener("click", async () => { + document + .querySelector("#sieve-editor-saving").classList.remove("d-none"); + + await this.save(); + + document + .querySelector("#sieve-editor-saving").classList.add("d-none"); + }); + + document + .querySelector('.nav-item > a[href="#sieve-widget-editor"]') + .addEventListener('show.bs.tab', async (e) => { + + if (!this.isTextEditor()) + return; + + e.preventDefault(); + + if (await this.switchToGraphicalEditor()) { + (new bootstrap.Tab('.nav-item > a[href="#sieve-widget-editor"]')).show(); + } + }); + + document + .querySelector('.nav-item > a[href="#sieve-widget-editor"]') + .addEventListener('shown.bs.tab', () => { + this.resize(); + }); + + window.addEventListener("resize", () => { + this.resize(); + }); + + document + .querySelector('.nav-item > a[href="#sieve-plaintext-editor"]') + .addEventListener('shown.bs.tab', () => { this.switchToTextEditor(); }); + + document + .querySelector('.nav-item > a[href="#sieve-content-settings"]') + .addEventListener('shown.bs.tab', () => { this.switchToSettings(); }); + + return this; + } + + /** + * Hides/Dismisses any error messages. + */ + hideErrorMessage() { + const elm = document.querySelector("#sieve-editor-error"); + if (elm !== null) + elm.remove(); + } + + /** + * Shows an error message. + * + * @param {string} message + * the error message to show. + */ + async showErrorMessage(message) { + + const content = await (new SieveTemplate()).load("./editor/editor.error.save.html"); + + content.querySelector(".sieve-editor-error-msg").textContent = message; + + this.hideErrorMessage(); + + document.querySelector("#sieve-editor-errors").append(content); + + // eslint-disable-next-line no-new + new bootstrap.Alert(content); + + this.resize(); + } + + /** + * Checks if the current editor has unsaved changes + * + * @returns {boolean} + * true in case the editor contains unsaved changes. + */ + async hasChanged() { + return (await this.getCurrentEditor().getChecksum() !== this.checksum); + } + + /** + * Loads the sieve script into the editor. + * All undo history will be flushed. + * + * @returns {boolean} + * true in case the script could be loaded otherwise false. + */ + async load() { + + const editor = this.getCurrentEditor(); + + await editor.setScript(await this.loadScript()); + + this.checksum = await editor.getChecksum(); + + editor.clearHistory(); + editor.focus(); + + return true; + } + + /** + * Saves the sieve script currently loaded into the editor. + * + * @returns {boolean} + * true in case the script could be saved otherwise false. + */ + async save() { + + if (!await this.hasChanged()) + return this; + + try { + + const editor = this.getCurrentEditor(); + + await this.saveScript( + await editor.getScript()); + + this.checksum = await editor.getChecksum(); + + this.hideErrorMessage(); + } catch (ex) { + this.showErrorMessage(ex.toString()); + return false; + } + + return true; + } + + /** + * Checks the script on the server for syntax errors. + * + * @param {string} [script] + * the script to check if omitted the current editors + * script will be used. + * + * @returns {undefined|string} + * in case of an script error, the error message + * otherwise null + */ + async checkScript(script) { + if (script === undefined || script === null) + script = await this.getCurrentEditor().getScript(); + + return await super.checkScript(script); + } + + /** + * Imports a sieve script from a file + */ + async importScript() { + + const script = await super.importScript(); + + if (typeof (script) === "undefined") + return; + + await this.getCurrentEditor().setScript(script); + + // The dialog stole our focus... + this.getCurrentEditor().focus(); + } + + /** + * Exports the script to a file + */ + async exportScript() { + await super.exportScript(await this.getCurrentEditor().getScript()); + + // The dialog stole our focus... + this.getCurrentEditor().focus(); + } + + /** + * Switches to the text editor. + * It transfers the script from the graphical to the text editor. + * + * @returns {boolean} + * true in case the editor was changed otherwise false. + */ + async switchToTextEditor() { + + document.querySelector("#sieve-editor-save").classList.remove("d-none"); + document.querySelector("#sieve-editor-toolbar").classList.remove("d-none"); + document.querySelector("#sieve-plaintext-editor-toolbar").classList.remove("d-none"); + + if (this.isTextEditor()) { + this.getTextEditor().focus(); + return true; + } + + // We keep the history intact so that undo works. + // The set script would be just like an edit.. + await this.getTextEditor().setScript( + await this.getGraphicalEditor().getScript()); + + this.isTextEditor(true); + + this.getTextEditor().focus(); + + return true; + } + + /** + * Switches the graphical editor. + * It transfers the script from the text editor to the graphical. + * It is only possible to switch to the graphical editor, + * when the script is free of syntax errors. + * + * @returns {boolean} + * true in case the editor was changed otherwise false. + */ + async switchToGraphicalEditor() { + + document.querySelector("#sieve-editor-save").classList.remove("d-none"); + document.querySelector("#sieve-editor-toolbar").classList.add("d-none"); + document.querySelector("#sieve-plaintext-editor-toolbar").classList.add("d-none"); + + if (!this.isTextEditor()) + return true; + + try { + await this.getGraphicalEditor().setScript( + await this.getTextEditor().getScript()); + } catch (ex) { + await this.switchToTextEditor(); + this.showErrorMessage(`Switching to Graphical editor failed ${ex}`); + return false; + } + + this.isTextEditor(false); + return true; + } + + /** + * Switches to the settings tab. + */ + switchToSettings() { + document.querySelector("#sieve-editor-toolbar").classList.add("d-none"); + document.querySelector("#sieve-editor-save").classList.add("d-none"); + } + + /** + * Gets the currently active editor type and optionally + * also sets the editor type. + * + * @param {boolean} [value] + * optional. If set to true the text editor will be enabled. + * Setting it to false will switch to the graphical editor. + * + * @returns {boolean} + * true in case the text editor is the current editor. + * Otherwise false. + */ + isTextEditor(value) { + + const elm = document.querySelector('.nav-item > a[href="#sieve-widget-editor"]'); + + if (value === true || value === false) + elm.dataset.currentEditor = (!value).toString(); + + return !(elm.dataset.currentEditor === "true"); + } + + /** + * Returns a reference to the currently active editor. + * Which is either the plain text editor or the rich text editor. + * + * @returns {SieveAbstractEditorUI} the currently active editor. + */ + getCurrentEditor() { + if (this.isTextEditor()) + return this.getTextEditor(); + + return this.getGraphicalEditor(); + } + + /** + * Returns the plain text editor. + * Keep in mind the editor might be hidden. + * + * @returns {SieveTextEditorUI} + * the plain text editor + */ + getTextEditor() { + return this.textEditor; + } + + /** + * Returns the graphical editor. + * Keep in mind the editor might be hidden. + * + * @returns {SieveGraphicalEditorUI} + * the graphical editor + */ + getGraphicalEditor() { + return this.graphicalEditor; + } + + /** + * Renders the current settings. + */ + async renderSettings() { + + const parent = document.querySelector("#sieve-content-settings"); + while (parent.firstChild) + parent.firstChild.remove(); + + await this.getTextEditor().renderSettings(); + // this.getGraphicalEditor().renderSettings(); + + parent.append( + await (new SieveTemplate()).load("./editor/editor.settings.defaults.html")); + + document.querySelector("#editor-settings-save-defaults") + .addEventListener("click", async () => { + await this.saveDefaultSettings(); + }); + + document.querySelector("#editor-settings-load-defaults") + .addEventListener("click", async () => { + await this.loadDefaultSettings(); + }); + } + + /** + * @inheritdoc + */ + async loadSettings() { + await this.getTextEditor().loadSettings(); + await this.getGraphicalEditor().loadSettings(); + + await this.renderSettings(); + } + + /** + * @inheritdoc + */ + async loadDefaultSettings() { + await this.getTextEditor().loadDefaultSettings(); + await this.getGraphicalEditor().loadDefaultSettings(); + + await this.renderSettings(); + } + + /** + * @inheritdoc + */ + async saveDefaultSettings() { + await this.getTextEditor().saveDefaultSettings(); + await this.getGraphicalEditor().saveDefaultSettings(); + } +} + +export { SieveEditorUI }; diff --git a/src/common/managesieve.ui/editor/SieveEditorController.js b/src/common/managesieve.ui/editor/SieveEditorController.js deleted file mode 100644 index 21e3243b..00000000 --- a/src/common/managesieve.ui/editor/SieveEditorController.js +++ /dev/null @@ -1,195 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - /* global SieveIpcClient */ - - /** - * An editor controller is the logical endpoint for all editor - * specific actions. It is triggered from the graphical editor - * as well as the text editor and interacts with the outside world - * like the sieve server or the operating system. - */ - class SieveEditorController { - - /** - * Creates a new instance - * - * @param {string} name - * the sieve scripts name - * @param {string} account - * the account id - */ - constructor(name, account) { - this.name = name; - this.account = account; - } - - /** - * Loads the script into the editor. - * - * @returns {string} - * the script to be loaded. - */ - async loadScript() { - return await this.send("script-get", this.name); - } - - /** - * Saves the current script. - * - * @param {string} script - * the script's content. - */ - async saveScript(script) { - await this.send("script-save", { "name": this.name, "script": script }); - } - - /** - * Checks if the given script is free of syntax errors. - * @param {string} script - * the script to be checked - * @returns {undefined|string} - * either undefined in case the script is ok or an error string otherwise. - */ - async checkScript(script) { - return await this.send("script-check", script); - } - - /** - * Sets the current clipboard content. - * - * @param {string} data - * the data to copy to the clipboard. - */ - async setClipboard(data) { - await this.send("copy", data); - } - - /** - * Gets the current clipboard content. - * - * @returns {string} - * the current clipboard content - */ - async getClipboard() { - return await this.send("paste"); - } - - /** - * Imports the given script from a file. - * - * @returns {string} - * the imported sieve script. - */ - async importScript() { - return await this.send("script-import"); - } - - /** - * Exports the given script into a file. - * @param {string} script - * the sieve content to be exported - */ - async exportScript(script) { - await this.send("script-export", { "name": this.name, "script": script }); - } - - /** - * Reruns the server side capabilities for the given account - * @returns {object} - * the server's capabilities. - */ - async getCapabilities() { - return await this.send("account-capabilities"); - } - - /** - * Sets a preference value. - * - * @param {string} name - * the preferences unique name. - * @param {object} value - * the value which should be set. - */ - async setPreference(name, value) { - await this.send("set-preference", { "key": name, "value": value }); - } - - /** - * Sets a default value for a preference. - * - * @param {string} name - * the preferences unique name. - * @param {object} value - * the value which should be set as default. - */ - async setDefaultPreference(name, value) { - await this.send("set-default-preference", { "key": name, "value": value }); - } - - /** - * Gets a preference value. - * - * @param {string} name - * the preferences unique name - * @returns {object} - * the requested value. - */ - async getPreference(name) { - return await this.send("get-preference", name); - } - - /** - * Gets the default value for a preference. - * - * @param {string} name - * the preferences unique name - * @returns {object} - * the preference's default value. - */ - async getDefaultPreference(name) { - return await this.send("get-default-preference", name); - } - - /** - * Executes an action on the communication process. - * - * @param {string} action - * the actions unique name - * @param {object} [payload] - * th payload which should be send - * @returns {Promise<object>} - * the result received for this action. - */ - async send(action, payload) { - - if (typeof (payload) === "undefined" || payload === null) - payload = {}; - - if (typeof (payload) !== "object") - payload = { "data": payload }; - - payload["account"] = this.account; - - return await SieveIpcClient.sendMessage("core", action, payload); - } - } - - if (typeof (module) !== "undefined" && module && module.exports) - module.exports.SieveEditorController = SieveEditorController; - else - exports.SieveEditorController = SieveEditorController; - -})(this); diff --git a/src/common/managesieve.ui/editor/SieveEditorController.mjs b/src/common/managesieve.ui/editor/SieveEditorController.mjs new file mode 100644 index 00000000..a9667f31 --- /dev/null +++ b/src/common/managesieve.ui/editor/SieveEditorController.mjs @@ -0,0 +1,186 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +import { SieveIpcClient } from "./../utils/SieveIpcClient.mjs"; + +/** + * An editor controller is the logical endpoint for all editor + * specific actions. It is triggered from the graphical editor + * as well as the text editor and interacts with the outside world + * like the sieve server or the operating system. + */ +class SieveEditorController { + + /** + * Creates a new instance + * + * @param {string} name + * the sieve scripts name + * @param {string} account + * the account id + */ + constructor(name, account) { + this.name = name; + this.account = account; + } + + /** + * Loads the script into the editor. + * + * @returns {string} + * the script to be loaded. + */ + async loadScript() { + return await this.send("script-get", this.name); + } + + /** + * Saves the current script. + * + * @param {string} script + * the script's content. + */ + async saveScript(script) { + await this.send("script-save", { "name": this.name, "script": script }); + } + + /** + * Checks if the given script is free of syntax errors. + * @param {string} script + * the script to be checked + * @returns {undefined|string} + * either undefined in case the script is ok or an error string otherwise. + */ + async checkScript(script) { + return await this.send("script-check", script); + } + + /** + * Sets the current clipboard content. + * + * @param {string} data + * the data to copy to the clipboard. + */ + async setClipboard(data) { + await this.send("copy", data); + } + + /** + * Gets the current clipboard content. + * + * @returns {string} + * the current clipboard content + */ + async getClipboard() { + return await this.send("paste"); + } + + /** + * Imports the given script from a file. + * + * @returns {string} + * the imported sieve script. + */ + async importScript() { + return await this.send("script-import"); + } + + /** + * Exports the given script into a file. + * @param {string} script + * the sieve content to be exported + */ + async exportScript(script) { + await this.send("script-export", { "name": this.name, "script": script }); + } + + /** + * Reruns the server side capabilities for the given account + * @returns {object} + * the server's capabilities. + */ + async getCapabilities() { + return await this.send("account-capabilities"); + } + + /** + * Sets a preference value. + * + * @param {string} name + * the preferences unique name. + * @param {object} value + * the value which should be set. + */ + async setPreference(name, value) { + await this.send("set-preference", { "key": name, "value": value }); + } + + /** + * Sets a default value for a preference. + * + * @param {string} name + * the preferences unique name. + * @param {object} value + * the value which should be set as default. + */ + async setDefaultPreference(name, value) { + await this.send("set-default-preference", { "key": name, "value": value }); + } + + /** + * Gets a preference value. + * + * @param {string} name + * the preferences unique name + * @returns {object} + * the requested value. + */ + async getPreference(name) { + return await this.send("get-preference", name); + } + + /** + * Gets the default value for a preference. + * + * @param {string} name + * the preferences unique name + * @returns {object} + * the preference's default value. + */ + async getDefaultPreference(name) { + return await this.send("get-default-preference", name); + } + + /** + * Executes an action on the communication process. + * + * @param {string} action + * the actions unique name + * @param {object} [payload] + * th payload which should be send + * @returns {Promise<object>} + * the result received for this action. + */ + async send(action, payload) { + + if (typeof (payload) === "undefined" || payload === null) + payload = {}; + + if (typeof (payload) !== "object") + payload = { "data": payload }; + + payload["account"] = this.account; + + return await SieveIpcClient.sendMessage("core", action, payload); + } +} + +export { SieveEditorController }; diff --git a/src/common/managesieve.ui/editor/editor.error.save.html b/src/common/managesieve.ui/editor/editor.error.save.html index ab3447a3..e62b5fa6 100644 --- a/src/common/managesieve.ui/editor/editor.error.save.html +++ b/src/common/managesieve.ui/editor/editor.error.save.html @@ -1,5 +1,5 @@ <div id="sieve-editor-error" class="alert alert-danger alert-dismissible fade show my-0" role="alert"> - <button type="button" class="btn-close" data-dismiss="alert" aria-label="Close"></button> + <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <strong data-i18n="editor.error.save.title"></strong> <span data-i18n="editor.error.save.description" class="sieve-editor-error-msg"></span> </div>
\ No newline at end of file diff --git a/src/common/managesieve.ui/editor/editor.tpl b/src/common/managesieve.ui/editor/editor.html index 8de2e532..5572f7ca 100644 --- a/src/common/managesieve.ui/editor/editor.tpl +++ b/src/common/managesieve.ui/editor/editor.html @@ -5,32 +5,32 @@ <div class="card-header d-flex justify-content-between py-0"> <ul class="nav nav-tabs card-header-tabs my-0 pt-3" role="tablist"> <li class="nav-item"> - <a class="nav-link" data-i18n="editor.script" data-toggle="tab" href="#sieve-widget-editor" + <a class="nav-link" data-i18n="editor.script" data-bs-toggle="tab" href="#sieve-widget-editor" role="tab"></a> </li> <li class="nav-item"> - <a class="nav-link active" data-i18n="editor.source" data-toggle="tab" href="#sieve-plaintext-editor" + <a class="nav-link active" data-i18n="editor.source" data-bs-toggle="tab" href="#sieve-plaintext-editor" role="tab"></a> </li> <li class="nav-item"> - <a id="sieve-tab-settings" data-i18n="editor.settings" class="nav-link" data-toggle="tab" + <a id="sieve-tab-settings" data-i18n="editor.settings" class="nav-link" data-bs-toggle="tab" href="#sieve-content-settings" role="tab"></a> </li> </ul> <div class="align-self-center"> - <button type="button" class="btn btn-sm btn-outline-secondary mr-1" id="sieve-editor-save"> + <button type="button" class="btn btn-sm btn-outline-secondary me-1" id="sieve-editor-save"> <span id="sieve-editor-saving" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span> <span data-i18n="editor.save"></span> </button> - <a class="btn btn-sm btn-outline-info mr-1 " href="https://www.paypal.com/paypalme2/thsmi" target="_blank" + <a class="btn btn-sm btn-outline-info me-1 " href="https://www.paypal.com/paypalme2/thsmi" target="_blank" role="button" data-i18n="editor.donate"></a> <div id="sieve-editor-settings" class="btn-group dropdown"> - <a class="btn btn-sm btn-outline-secondary dropdown-toggle" role="button" data-toggle="dropdown" + <a class="btn btn-sm btn-outline-secondary dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> ☰ </a> - <div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuLink"> + <div class="dropdown-menu dropdown-menu-end" aria-labelledby="dropdownMenuLink"> <a data-i18n="editor.menu.settings" class="dropdown-item sieve-editor-settings-show"></a> <a data-i18n="editor.menu.reload" class="dropdown-item" href="javascript:window.location.reload(true)"></a> diff --git a/src/common/managesieve.ui/editor/editor.settings.defaults.tpl b/src/common/managesieve.ui/editor/editor.settings.defaults.html index 1b1b9fb3..83cf2cfa 100644 --- a/src/common/managesieve.ui/editor/editor.settings.defaults.tpl +++ b/src/common/managesieve.ui/editor/editor.settings.defaults.html @@ -4,6 +4,6 @@ <p data-i18n="editor.defaults.description"></p> <div> <a id="editor-settings-save-defaults" data-i18n="editor.defaults.save" class="btn btn-sm btn-outline-secondary" role="button"></a> - <a id="editor-settings-load-defaults" data-i18n="editor.defaults.load" class="btn btn-sm btn-outline-secondary mr-1" role="button"></a> + <a id="editor-settings-load-defaults" data-i18n="editor.defaults.load" class="btn btn-sm btn-outline-secondary me-1" role="button"></a> </div> </div>
\ No newline at end of file diff --git a/src/common/managesieve.ui/editor/graphical/SieveGraphicalEditor.js b/src/common/managesieve.ui/editor/graphical/SieveGraphicalEditor.js deleted file mode 100644 index 93c4ae79..00000000 --- a/src/common/managesieve.ui/editor/graphical/SieveGraphicalEditor.js +++ /dev/null @@ -1,69 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - /* global SieveAbstractEditorUI */ - - // FIXME should use an IPC instead of talking directly to the iframe... - - /** - * - */ - class SieveGraphicalEditorUI extends SieveAbstractEditorUI { - - /** - * Creates a new graphical editor UI. - * - * @param {SieveEditorController} controller - * The controller which is assigned to this editor. - */ - constructor(controller) { - super(controller); - this.id = "sieve-widget-editor"; - } - - /** - * @inheritdoc - */ - async render() { - } - - /** - * @inheritdoc - */ - async setScript(script) { - - const capabilities = await this.getController().getCapabilities(); - // set script content... - document.getElementById(this.id) - .contentWindow - .setSieveScript(script, JSON.stringify(capabilities.extensions)); - } - - /** - * @inheritdoc - */ - getScript() { - return document.getElementById(this.id) - .contentWindow - .getSieveScript(); - } - } - - if (typeof (module) !== "undefined" && module && module.exports) - module.exports.SieveGraphicalEditorUI = SieveGraphicalEditorUI; - else - exports.SieveGraphicalEditorUI = SieveGraphicalEditorUI; - -})(this); diff --git a/src/common/managesieve.ui/editor/graphical/SieveGraphicalEditor.mjs b/src/common/managesieve.ui/editor/graphical/SieveGraphicalEditor.mjs new file mode 100644 index 00000000..1e4db769 --- /dev/null +++ b/src/common/managesieve.ui/editor/graphical/SieveGraphicalEditor.mjs @@ -0,0 +1,60 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +import { SieveAbstractEditorUI } from "./../SieveAbstractEditor.mjs"; + +// FIXME should use an IPC instead of talking directly to the iframe... + +/** + * + */ +class SieveGraphicalEditorUI extends SieveAbstractEditorUI { + + /** + * Creates a new graphical editor UI. + * + * @param {SieveEditorController} controller + * The controller which is assigned to this editor. + */ + constructor(controller) { + super(controller); + this.id = "sieve-widget-editor"; + } + + /** + * @inheritdoc + */ + async render() { + } + + /** + * @inheritdoc + */ + async setScript(script) { + + const capabilities = await this.getController().getCapabilities(); + // set script content... + document.querySelector(`#${this.id}`) + .contentWindow + .setSieveScript(script, JSON.stringify(capabilities.extensions)); + } + + /** + * @inheritdoc + */ + getScript() { + return document.querySelector(`#${this.id}`) + .contentWindow + .getSieveScript(); + } +} + +export { SieveGraphicalEditorUI }; diff --git a/src/common/managesieve.ui/editor/text/SieveTextEditor.js b/src/common/managesieve.ui/editor/text/SieveTextEditor.js deleted file mode 100644 index c0309d0c..00000000 --- a/src/common/managesieve.ui/editor/text/SieveTextEditor.js +++ /dev/null @@ -1,760 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - /* global CodeMirror */ - /* global SieveTemplate */ - /* global SieveAbstractEditorUI */ - - const COMPILE_DELAY = 500; - const EDITOR_SCROLL_INTO_VIEW_OFFSET = 200; - - /** - * An text editor ui for sieve scripts. - */ - class SieveTextEditorUI extends SieveAbstractEditorUI { - - /** - * Creates a new text editor UI. - * - * @param {SieveEditorController} controller - * The controller which is assigned to this editor. - * @param {string} [id] - * An optional id, which points to a the textbox, which will be converted - * into a code mirror input. In case it is omitted the id "code" will be used. - */ - constructor(controller, id) { - - super(controller); - - if (typeof (id) === "undefined" || id === null) - this.id = "code"; - - this.syntaxCheckEnabled = false; - this.timeout = null; - - this.cm = null; - - this.activeLine = null; - - this.changed = false; - } - - /** - * Renders the text editors settings - */ - async renderSettings() { - - - const loader = new SieveTemplate(); - - // Syntax Checks - document - .querySelector("#sieve-content-settings") - .appendChild(await loader.load("./editor/text/editor.settings.syntax.tpl")); - - document - .querySelector("#sieve-editor-settings-synatxcheck") - .addEventListener("click", async () => { - - if (document.querySelector("#sieve-editor-settings-synatxcheck").checked === true) - await this.enableSyntaxCheck(); - else - await this.disableSyntaxCheck(); - }); - - document.querySelector("#sieve-editor-settings-synatxcheck") - .checked = this.isSyntaxCheckEnabled(); - - // Indentation - document - .querySelector("#sieve-content-settings") - .appendChild(await loader.load("./editor/text/editor.settings.indentation.tpl")); - - // Indentation width... - document - .querySelector("#editor-settings-indentation-width") - .addEventListener("change", async () => { - await this.setIndentWidth( - document.querySelector("#editor-settings-indentation-width").value); - }); - - document.querySelector("#editor-settings-indentation-width") - .value = this.getIndentWidth(); - - // Indentation policy... - document - .querySelector("#editor-settings-indentation-policy-spaces") - .addEventListener("change", async () => { await this.setIndentWithTabs(false); }); - - document - .querySelector("#editor-settings-indentation-policy-tabs") - .addEventListener("change", async () => { await this.setIndentWithTabs(true); }); - - if (this.getIndentWithTabs()) - document.querySelector("#editor-settings-indentation-policy-tabs").checked = true; - else - document.querySelector("#editor-settings-indentation-policy-spaces").checked = true; - - // Tabulator width... - document - .querySelector("#editor-settings-tabulator-width") - .addEventListener("change", async () => { - await this.setTabWidth( - document.querySelector("#editor-settings-tabulator-width").value); - }); - - document.querySelector("#editor-settings-tabulator-width") - .value = this.getTabWidth(); - } - - /** - * @inheritdoc - */ - async render() { - - const loader = new SieveTemplate(); - - const editor = document.querySelector("#sieve-plaintext-editor"); - while (editor.firstChild) - editor.removeChild(editor.firstChild); - - editor.appendChild( - await loader.load("./editor/text/editor.plaintext.html")); - - this.cm = CodeMirror.fromTextArea(document.getElementById(this.id), { - lineNumbers: true, - lineWrapping: true, - - theme: "eclipse", - matchBrackets: true, - - inputStyle: "contenteditable" - }); - - this.cm.on("renderLine", (cm, line, elt) => { this.onRenderLine(cm, line, elt); }); - this.cm.on("change", () => { this.onChange(); }); - this.cm.on("cursorActivity", () => { this.onActiveLineChange(); }); - - this.cm.refresh(); - - // Configure tab handling... - this.cm.setOption("extraKeys", { - "Tab": function (cm) { - - if (cm.somethingSelected()) { - const sel = cm.getSelection("\n"); - // Indent only if there are multiple lines selected, or if the selection spans a full line - if (sel.length > 0 && (sel.indexOf("\n") > -1 || sel.length === cm.getLine(cm.getCursor().line).length)) { - cm.indentSelection("add"); - return; - } - } - - if (cm.options.indentWithTabs) - cm.execCommand("insertTab"); - else - cm.execCommand("insertSoftTab"); - }, - "Shift-Tab": function (cm) { - cm.indentSelection("subtract"); - } - }); - - const toolbar = document.querySelector("#sieve-editor-toolbar"); - toolbar.appendChild( - await loader.load("./editor/text/editor.plaintext.toolbar.html")); - - document - .querySelector("#sieve-editor-undo") - .addEventListener("click", () => { this.undo(); }); - - document - .querySelector("#sieve-editor-redo") - .addEventListener("click", () => { this.redo(); }); - - document - .querySelector("#sieve-editor-cut") - .addEventListener("click", () => { this.cut(); }); - - document - .querySelector("#sieve-editor-copy") - .addEventListener("click", () => { this.copy(); }); - - document - .querySelector("#sieve-editor-paste") - .addEventListener("click", () => { this.paste(); }); - - document - .querySelector("#sieve-editor-find") - .addEventListener("click", () => { - const token = document.querySelector("#sieve-editor-txt-find").value; - - const isReverse = document.querySelector("#sieve-editor-backward").checked; - const isCaseSensitive = document.querySelector("#sieve-editor-casesensitive").checked; - - this.find(token, isCaseSensitive, isReverse); - }); - - document - .querySelector("#sieve-editor-replace") - .addEventListener("click", () => { - const oldToken = document.querySelector("#sieve-editor-txt-find").value; - const newToken = document.querySelector("#sieve-editor-txt-replace").value; - - const isReverse = document.querySelector("#sieve-editor-backward").checked; - const isCaseSensitive = document.querySelector("#sieve-editor-casesensitive").checked; - - if (oldToken === "") - return; - - this.replace(oldToken, newToken, isCaseSensitive, isReverse); - }); - - - document - .querySelector("#sieve-editor-replace-replace") - .addEventListener("click", () => { - document.querySelector("#sieve-editor-find-toolbar").classList.toggle("d-none"); - }); - - await this.renderSettings(); - } - - /** - * Returns the editor change status. - * - * @returns {boolean} - * true in case the document was changed otherwise false. - */ - hasChanged() { - return this.changed; - } - - /** - * @inheritdoc - */ - async setScript(script) { - // Load a new script. It will discard the current script - // the cursor position is reset to defaults. - - this.cm.setValue(script); - this.cm.setCursor({ line: 0, ch: 0 }); - - this.cm.refresh(); - - // ensure the active line cursor changed... - // onActiveLineChange(); - } - - /** - * @inheritdoc - */ - getScript() { - - this.focus(); - - const script = this.cm.getValue(); - - // ... and ensure the line endings are sanitized - // eslint-disable-next-line no-control-regex - return script.replace(/\r\n|\r|\n|\u0085|\u000C|\u2028|\u2029/g, "\r\n"); - } - - /** - * @inheritdoc - */ - focus() { - if (this.cm) - this.cm.focus(); - } - - /** - * @inheritdoc - */ - clearHistory() { - this.cm.clearHistory(); - } - - /** - * Checks the current script for syntax errors - */ - async checkScript() { - - const errors = await this.getController().checkScript(await this.getScript()); - - if (errors && errors !== "") - this.showSyntaxErrors(errors); - else - this.hideSyntaxErrors(); - } - - /** - * Undoes the last input - */ - undo() { - this.cm.undo(); - this.cm.focus(); - } - - /** - * Redos the last input - */ - redo() { - this.cm.redo(); - this.cm.focus(); - } - - /** - * Cuts the currently selected text. - */ - async cut() { - await this.copy(); - this.cm.replaceSelection(""); - - this.cm.focus(); - } - - /** - * Copies the currently selected text. - */ - async copy() { - const data = this.cm.getSelection(); - - await this.getController().setClipboard(data); - - this.cm.focus(); - } - - /** - * Pastes the clipboard content into the editor. - */ - async paste() { - const data = await this.getController().getClipboard(); - this.cm.replaceSelection(data); - - this.cm.focus(); - } - - /** - * Gets the selection begin - * - * @param {boolean} isReverse - * if true the selection is handled in reverse order. - * which means the selection start gets the selections end and vice versa. - * @returns {int} - * the current start position. - */ - getSelectionStart(isReverse) { - - const start = this.cm.getCursor(true); - const end = this.cm.getCursor(false); - - if (isReverse) { - if (start.line < end.line) - return start; - - if (start.line > end.line) - return end; - - // start.line == end.line - if (start.ch > end.ch) - return end; - - return start; - } - - - if (start.line > end.line) - return start; - - if (start.line < end.line) - return end; - - // start.line == end.line - if (start.ch > end.ch) - return start; - - return end; - - } - - /** - * Finds the specified token within the editor. - * - * @param {string} token - * the string to find. - * @param {boolean} [isCaseSensitive] - * if true the search is case sensitive. - * @param {boolean} [isReverse] - * if true the search will be in reverse direction. - * @returns {boolean} - * true in case the the string was found otherwise false. - */ - find(token, isCaseSensitive, isReverse) { - - // Fix optional parameters... - if (typeof (isCaseSensitive) === "undefined" || isCaseSensitive === null) - isCaseSensitive = false; - - if (typeof (isReverse) === "undefined" || isReverse === null) - isReverse = false; - - let cursor = this.cm.getSearchCursor( - token, - this.getSelectionStart(isReverse), - !isCaseSensitive); - - if (!cursor.find(isReverse)) { - // warp search at top or bottom - cursor = this.cm.getSearchCursor( - token, - isReverse ? { line: this.cm.lineCount() - 1 } : { line: 0, ch: 0 }, - !isCaseSensitive); - - if (!cursor.find(isReverse)) - return false; - } - - if (isReverse) - this.cm.setSelection(cursor.from(), cursor.to()); - else - this.cm.setSelection(cursor.to(), cursor.from()); - - this.cm.scrollIntoView(cursor.to(), EDITOR_SCROLL_INTO_VIEW_OFFSET); - - return true; - } - - /** - * Checks if the specified token is selected. - * - * @param {string} token - * the token - * @param {boolean} isCaseSensitive - * true in case the check should be case insensitive. - * @returns {boolean} - * true in case the token was found otherwise false. - */ - isSelected(token, isCaseSensitive) { - let selection = this.cm.getSelection(); - - if (isCaseSensitive) { - selection = selection.toLowerCase(); - token = token.toLocaleLowerCase(); - } - - if (selection !== token) - return false; - - return true; - } - - /** - * Replaces the old token with the new token. - * - * @param {string} oldToken - * the old token which should be replaced - * @param {string} newToken - * the new token - * @param {boolean} [isCaseSensitive] - * if true the search is case sensitive. - * @param {boolean} [isReverse] - * if true the search will be in reverse direction. - * @returns {boolean} - * true if the string was replaced, otherwise false. - */ - replace(oldToken, newToken, isCaseSensitive, isReverse) { - - // Fix optional parameters... - if (typeof (isCaseSensitive) === "undefined" || isCaseSensitive === null) - isCaseSensitive = false; - - if (typeof (isReverse) === "undefined" || isReverse === null) - isReverse = false; - - if (this.isSelected(oldToken, isCaseSensitive) === false) { - if (this.find(oldToken, isCaseSensitive, isReverse) === false) - return false; - } - - this.cm.replaceSelection(newToken); - - return true; - } - - /** - * Callback handler for code mirror. Do not invoke unless you know what you are doing. - * - * @param {CodeMirror} cm - * a reference to the code mirror instance - * @param {LineHandle} line - * the current line - * @param {Element} element - * the dom element which represents the line - */ - onRenderLine(cm, line, element) { - const charWidth = this.cm.defaultCharWidth(); - const basePadding = 4; - - const off = CodeMirror.countColumn(line.text, null, cm.getOption("tabSize")) * charWidth; - element.style.textIndent = "-" + off + "px"; - element.style.paddingLeft = (basePadding + off) + "px"; - } - - /** - * On Change callback handler for codemirror - * Do not invoke unless you know what you are doing. - */ - onChange() { - - if (this.syntaxCheckEnabled === false) - return; - - // reset the timer... - if (this.timeout !== null) { - clearTimeout(this.timeout); - this.timeout = null; - } - - this.timeout = setTimeout(() => { this.checkScript(); }, COMPILE_DELAY); - } - - /** - * On Active Line Change callback handler for codemirror. - * Do not invoke unless you know what you are doing. - */ - onActiveLineChange() { - const currentLine = this.cm.getLineHandle(this.cm.getCursor().line); - - if (currentLine === this.activeLine) - return; - - if (this.activeLine) - this.cm.removeLineClass(this.activeLine, "background", "activeline"); - - this.activeLine = this.cm.addLineClass(currentLine, "background", "activeline"); - } - - /** - * Enables checking for syntax errors - */ - async enableSyntaxCheck() { - this.syntaxCheckEnabled = true; - this.checkScript(); - - this.focus(); - - await this.getController().setPreference("syntax-check", this.syntaxCheckEnabled); - } - - /** - * Disables checking for syntax errors - */ - async disableSyntaxCheck() { - this.syntaxCheckEnabled = false; - this.hideSyntaxErrors(); - - this.focus(); - - await this.getController().setPreference("syntax-check", this.syntaxCheckEnabled); - - // reset the timer... - if (this.timeout === null) - return; - - clearTimeout(this.timeout); - this.timeout = null; - } - - /** - * Checks if syntax checking is enabled - * @returns {boolean} - * true in case syntax check is enabled otherwise false - */ - isSyntaxCheckEnabled() { - return this.syntaxCheckEnabled; - } - - /** - * Shows a message box with the given syntax errors - * @param {string} errors - * the errors which should be displayed - */ - showSyntaxErrors(errors) { - const msg = document.querySelector("#sieve-editor-msg"); - msg.style.display = ''; - - const details = msg.querySelector(".sieve-editor-msg-details"); - while (details.firstChild) - details.removeChild(details.firstChild); - - details.textContent = errors; - } - - /** - * Hides the syntax errors. - */ - hideSyntaxErrors() { - document.querySelector("#sieve-editor-msg").style.display = 'none'; - } - - /** - * Sets the editors indentation width. - * - * @param {int} width - * the indentation width in characters - * @returns {SieveEditorUI} - * a self reference - */ - async setIndentWidth(width) { - width = parseInt(width, 10); - - if (isNaN(width)) - throw new Error("Invalid Indent width"); - - this.cm.setOption("indentUnit", width); - await this.getController().setPreference("indentation-width", width); - - return this; - } - - /** - * Returns the indentation width. - * - * @returns {int} - * the indentation width in characters. - */ - getIndentWidth() { - return this.cm.getOption("indentUnit"); - } - - /** - * Sets the indent policy. - * - * @param {boolean} useTabs - * if true tabs are used for indenting otherwise spaces are used. - * @returns {SieveEditorUI} - * a self reference - */ - async setIndentWithTabs(useTabs) { - this.cm.setOption("indentWithTabs", useTabs); - - await this.getController().setPreference("indentation-policy", useTabs); - - return this; - } - - /** - * Returns the indent policy. - * - * @returns {boolean} - * true in case tabs are used to indent. False if spaces are used. - */ - getIndentWithTabs() { - return this.cm.getOption("indentWithTabs"); - } - - /** - * Sets the editor's tabulator width and persists the changed value. - * - * @param {int} tabSize - * the tabulator width in characters - * @returns {SieveEditorUI} - * a self reference - */ - async setTabWidth(tabSize) { - tabSize = parseInt(tabSize, 10); - - if (isNaN(tabSize)) - throw new Error(`Invalid Tab width ${tabSize}`); - - this.cm.setOption("tabSize", tabSize); - - await this.getController().setPreference("tabulator-width", tabSize); - - return this; - } - - /** - * Gets the editor's tabulator width. - * @returns {int} - * the tabulator width in characters. - */ - getTabWidth() { - return this.cm.getOption("tabSize"); - } - - /** - * @inheritdoc - */ - async loadSettings() { - const tabWidth = await this.getController().getPreference("tabulator-width"); - await this.setTabWidth(tabWidth); - - const IndentWithTabs = await this.getController().getPreference("indentation-policy"); - await this.setIndentWithTabs(IndentWithTabs); - - const indentWidth = await this.getController().getPreference("indentation-width"); - await this.setIndentWidth(indentWidth); - - const syntaxCheck = await this.getController().getPreference("syntax-check"); - if (syntaxCheck === false || syntaxCheck === "false") - await this.disableSyntaxCheck(); - else - await this.enableSyntaxCheck(); - } - - /** - * @inheritdoc - */ - async loadDefaultSettings() { - const tabWidth = await this.getController().getDefaultPreference("tabulator-width"); - await this.setTabWidth(tabWidth); - - const IndentWithTabs = await this.getController().getDefaultPreference("indentation-policy"); - await this.setIndentWithTabs(IndentWithTabs); - - const indentWidth = await this.getController().getDefaultPreference("indentation-width"); - await this.setIndentWidth(indentWidth); - - const syntaxCheck = await this.getController().getDefaultPreference("syntax-check"); - if (syntaxCheck === false) - await this.disableSyntaxCheck(); - else - await this.enableSyntaxCheck(); - - await this.renderSettings(); - } - - /** - * @inheritdoc - */ - async saveDefaultSettings() { - await this.getController().setDefaultPreference("tabulator-width", this.getTabWidth()); - - await this.getController().setDefaultPreference("indentation-policy", this.getIndentWithTabs()); - await this.getController().setDefaultPreference("indentation-width", this.getIndentWidth()); - - await this.getController().setDefaultPreference("syntax-check", this.isSyntaxCheckEnabled()); - } - - } - - if (typeof (module) !== "undefined" && module && module.exports) - module.exports.SieveTextEditorUI = SieveTextEditorUI; - else - exports.SieveTextEditorUI = SieveTextEditorUI; - -})(this); diff --git a/src/common/managesieve.ui/editor/text/SieveTextEditor.mjs b/src/common/managesieve.ui/editor/text/SieveTextEditor.mjs new file mode 100644 index 00000000..49b87302 --- /dev/null +++ b/src/common/managesieve.ui/editor/text/SieveTextEditor.mjs @@ -0,0 +1,752 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +/* global CodeMirror */ + +import { SieveTemplate } from "./../../utils/SieveTemplate.mjs"; +import { SieveAbstractEditorUI } from "./../SieveAbstractEditor.mjs"; + +const COMPILE_DELAY = 500; +const EDITOR_SCROLL_INTO_VIEW_OFFSET = 200; + +/** + * An text editor ui for sieve scripts. + */ +class SieveTextEditorUI extends SieveAbstractEditorUI { + + /** + * Creates a new text editor UI. + * + * @param {SieveEditorController} controller + * The controller which is assigned to this editor. + * @param {string} [id] + * An optional id, which points to a the textbox, which will be converted + * into a code mirror input. In case it is omitted the id "code" will be used. + */ + constructor(controller, id) { + + super(controller); + + if (typeof (id) === "undefined" || id === null) + this.id = "code"; + + this.syntaxCheckEnabled = false; + this.timeout = null; + + this.cm = null; + + this.activeLine = null; + + this.changed = false; + } + + /** + * Renders the text editors settings + */ + async renderSettings() { + + + const loader = new SieveTemplate(); + + // Syntax Checks + document + .querySelector("#sieve-content-settings") + .append(await loader.load("./editor/text/editor.settings.syntax.html")); + + document + .querySelector("#sieve-editor-settings-synatxcheck") + .addEventListener("click", async () => { + + if (document.querySelector("#sieve-editor-settings-synatxcheck").checked === true) + await this.enableSyntaxCheck(); + else + await this.disableSyntaxCheck(); + }); + + document.querySelector("#sieve-editor-settings-synatxcheck") + .checked = this.isSyntaxCheckEnabled(); + + // Indentation + document + .querySelector("#sieve-content-settings") + .append(await loader.load("./editor/text/editor.settings.indentation.html")); + + // Indentation width... + document + .querySelector("#editor-settings-indentation-width") + .addEventListener("change", async () => { + await this.setIndentWidth( + document.querySelector("#editor-settings-indentation-width").value); + }); + + document.querySelector("#editor-settings-indentation-width") + .value = this.getIndentWidth(); + + // Indentation policy... + document + .querySelector("#editor-settings-indentation-policy-spaces") + .addEventListener("change", async () => { await this.setIndentWithTabs(false); }); + + document + .querySelector("#editor-settings-indentation-policy-tabs") + .addEventListener("change", async () => { await this.setIndentWithTabs(true); }); + + if (this.getIndentWithTabs()) + document.querySelector("#editor-settings-indentation-policy-tabs").checked = true; + else + document.querySelector("#editor-settings-indentation-policy-spaces").checked = true; + + // Tabulator width... + document + .querySelector("#editor-settings-tabulator-width") + .addEventListener("change", async () => { + await this.setTabWidth( + document.querySelector("#editor-settings-tabulator-width").value); + }); + + document.querySelector("#editor-settings-tabulator-width") + .value = this.getTabWidth(); + } + + /** + * @inheritdoc + */ + async render() { + + const loader = new SieveTemplate(); + + const editor = document.querySelector("#sieve-plaintext-editor"); + while (editor.firstChild) + editor.firstChild.remove(); + + editor.append( + await loader.load("./editor/text/editor.plaintext.html")); + + this.cm = CodeMirror.fromTextArea(document.querySelector(`#${this.id}`), { + lineNumbers: true, + lineWrapping: true, + + theme: "eclipse", + matchBrackets: true, + + inputStyle: "contenteditable" + }); + + this.cm.on("renderLine", (cm, line, elt) => { this.onRenderLine(cm, line, elt); }); + this.cm.on("change", () => { this.onChange(); }); + this.cm.on("cursorActivity", () => { this.onActiveLineChange(); }); + + this.cm.refresh(); + + // Configure tab handling... + this.cm.setOption("extraKeys", { + "Tab": function (cm) { + + if (cm.somethingSelected()) { + const sel = cm.getSelection("\n"); + // Indent only if there are multiple lines selected, or if the selection spans a full line + if (sel.length > 0 && (sel.includes("\n") || sel.length === cm.getLine(cm.getCursor().line).length)) { + cm.indentSelection("add"); + return; + } + } + + if (cm.options.indentWithTabs) + cm.execCommand("insertTab"); + else + cm.execCommand("insertSoftTab"); + }, + "Shift-Tab": function (cm) { + cm.indentSelection("subtract"); + } + }); + + const toolbar = document.querySelector("#sieve-editor-toolbar"); + toolbar.append( + await loader.load("./editor/text/editor.plaintext.toolbar.html")); + + document + .querySelector("#sieve-editor-undo") + .addEventListener("click", () => { this.undo(); }); + + document + .querySelector("#sieve-editor-redo") + .addEventListener("click", () => { this.redo(); }); + + document + .querySelector("#sieve-editor-cut") + .addEventListener("click", () => { this.cut(); }); + + document + .querySelector("#sieve-editor-copy") + .addEventListener("click", () => { this.copy(); }); + + document + .querySelector("#sieve-editor-paste") + .addEventListener("click", () => { this.paste(); }); + + document + .querySelector("#sieve-editor-find") + .addEventListener("click", () => { + const token = document.querySelector("#sieve-editor-txt-find").value; + + const isReverse = document.querySelector("#sieve-editor-backward").checked; + const isCaseSensitive = document.querySelector("#sieve-editor-casesensitive").checked; + + this.find(token, isCaseSensitive, isReverse); + }); + + document + .querySelector("#sieve-editor-replace") + .addEventListener("click", () => { + const oldToken = document.querySelector("#sieve-editor-txt-find").value; + const newToken = document.querySelector("#sieve-editor-txt-replace").value; + + const isReverse = document.querySelector("#sieve-editor-backward").checked; + const isCaseSensitive = document.querySelector("#sieve-editor-casesensitive").checked; + + if (oldToken === "") + return; + + this.replace(oldToken, newToken, isCaseSensitive, isReverse); + }); + + + document + .querySelector("#sieve-editor-replace-replace") + .addEventListener("click", () => { + document.querySelector("#sieve-editor-find-toolbar").classList.toggle("d-none"); + }); + + await this.renderSettings(); + } + + /** + * Returns the editor change status. + * + * @returns {boolean} + * true in case the document was changed otherwise false. + */ + hasChanged() { + return this.changed; + } + + /** + * @inheritdoc + */ + async setScript(script) { + // Load a new script. It will discard the current script + // the cursor position is reset to defaults. + + this.cm.setValue(script); + this.cm.setCursor({ line: 0, ch: 0 }); + + this.cm.refresh(); + + // ensure the active line cursor changed... + // onActiveLineChange(); + } + + /** + * @inheritdoc + */ + getScript() { + + this.focus(); + + const script = this.cm.getValue(); + + // ... and ensure the line endings are sanitized + // eslint-disable-next-line no-control-regex + return script.replace(/\r\n|\r|\n|\u0085|\u000C|\u2028|\u2029/g, "\r\n"); + } + + /** + * @inheritdoc + */ + focus() { + if (this.cm) + this.cm.focus(); + } + + /** + * @inheritdoc + */ + clearHistory() { + this.cm.clearHistory(); + } + + /** + * Checks the current script for syntax errors + */ + async checkScript() { + + const errors = await this.getController().checkScript(await this.getScript()); + + if (errors && errors !== "") + this.showSyntaxErrors(errors); + else + this.hideSyntaxErrors(); + } + + /** + * Undoes the last input + */ + undo() { + this.cm.undo(); + this.cm.focus(); + } + + /** + * Redos the last input + */ + redo() { + this.cm.redo(); + this.cm.focus(); + } + + /** + * Cuts the currently selected text. + */ + async cut() { + await this.copy(); + this.cm.replaceSelection(""); + + this.cm.focus(); + } + + /** + * Copies the currently selected text. + */ + async copy() { + const data = this.cm.getSelection(); + + await this.getController().setClipboard(data); + + this.cm.focus(); + } + + /** + * Pastes the clipboard content into the editor. + */ + async paste() { + const data = await this.getController().getClipboard(); + this.cm.replaceSelection(data); + + this.cm.focus(); + } + + /** + * Gets the selection begin + * + * @param {boolean} isReverse + * if true the selection is handled in reverse order. + * which means the selection start gets the selections end and vice versa. + * @returns {int} + * the current start position. + */ + getSelectionStart(isReverse) { + + const start = this.cm.getCursor(true); + const end = this.cm.getCursor(false); + + if (isReverse) { + if (start.line < end.line) + return start; + + if (start.line > end.line) + return end; + + // start.line == end.line + if (start.ch > end.ch) + return end; + + return start; + } + + + if (start.line > end.line) + return start; + + if (start.line < end.line) + return end; + + // start.line == end.line + if (start.ch > end.ch) + return start; + + return end; + + } + + /** + * Finds the specified token within the editor. + * + * @param {string} token + * the string to find. + * @param {boolean} [isCaseSensitive] + * if true the search is case sensitive. + * @param {boolean} [isReverse] + * if true the search will be in reverse direction. + * @returns {boolean} + * true in case the the string was found otherwise false. + */ + find(token, isCaseSensitive, isReverse) { + + // Fix optional parameters... + if (typeof (isCaseSensitive) === "undefined" || isCaseSensitive === null) + isCaseSensitive = false; + + if (typeof (isReverse) === "undefined" || isReverse === null) + isReverse = false; + + let cursor = this.cm.getSearchCursor( + token, + this.getSelectionStart(isReverse), + !isCaseSensitive); + + if (!cursor.find(isReverse)) { + // warp search at top or bottom + cursor = this.cm.getSearchCursor( + token, + isReverse ? { line: this.cm.lineCount() - 1 } : { line: 0, ch: 0 }, + !isCaseSensitive); + + if (!cursor.find(isReverse)) + return false; + } + + if (isReverse) + this.cm.setSelection(cursor.from(), cursor.to()); + else + this.cm.setSelection(cursor.to(), cursor.from()); + + this.cm.scrollIntoView(cursor.to(), EDITOR_SCROLL_INTO_VIEW_OFFSET); + + return true; + } + + /** + * Checks if the specified token is selected. + * + * @param {string} token + * the token + * @param {boolean} isCaseSensitive + * true in case the check should be case insensitive. + * @returns {boolean} + * true in case the token was found otherwise false. + */ + isSelected(token, isCaseSensitive) { + let selection = this.cm.getSelection(); + + if (isCaseSensitive) { + selection = selection.toLowerCase(); + token = token.toLocaleLowerCase(); + } + + if (selection !== token) + return false; + + return true; + } + + /** + * Replaces the old token with the new token. + * + * @param {string} oldToken + * the old token which should be replaced + * @param {string} newToken + * the new token + * @param {boolean} [isCaseSensitive] + * if true the search is case sensitive. + * @param {boolean} [isReverse] + * if true the search will be in reverse direction. + * @returns {boolean} + * true if the string was replaced, otherwise false. + */ + replace(oldToken, newToken, isCaseSensitive, isReverse) { + + // Fix optional parameters... + if (typeof (isCaseSensitive) === "undefined" || isCaseSensitive === null) + isCaseSensitive = false; + + if (typeof (isReverse) === "undefined" || isReverse === null) + isReverse = false; + + if (this.isSelected(oldToken, isCaseSensitive) === false) { + if (this.find(oldToken, isCaseSensitive, isReverse) === false) + return false; + } + + this.cm.replaceSelection(newToken); + + return true; + } + + /** + * Callback handler for code mirror. Do not invoke unless you know what you are doing. + * + * @param {CodeMirror} cm + * a reference to the code mirror instance + * @param {LineHandle} line + * the current line + * @param {Element} element + * the dom element which represents the line + */ + onRenderLine(cm, line, element) { + const charWidth = this.cm.defaultCharWidth(); + const basePadding = 4; + + const off = CodeMirror.countColumn(line.text, null, cm.getOption("tabSize")) * charWidth; + element.style.textIndent = "-" + off + "px"; + element.style.paddingLeft = (basePadding + off) + "px"; + } + + /** + * On Change callback handler for codemirror + * Do not invoke unless you know what you are doing. + */ + onChange() { + + if (this.syntaxCheckEnabled === false) + return; + + // reset the timer... + if (this.timeout !== null) { + clearTimeout(this.timeout); + this.timeout = null; + } + + this.timeout = setTimeout(() => { this.checkScript(); }, COMPILE_DELAY); + } + + /** + * On Active Line Change callback handler for codemirror. + * Do not invoke unless you know what you are doing. + */ + onActiveLineChange() { + const currentLine = this.cm.getLineHandle(this.cm.getCursor().line); + + if (currentLine === this.activeLine) + return; + + if (this.activeLine) + this.cm.removeLineClass(this.activeLine, "background", "activeline"); + + this.activeLine = this.cm.addLineClass(currentLine, "background", "activeline"); + } + + /** + * Enables checking for syntax errors + */ + async enableSyntaxCheck() { + this.syntaxCheckEnabled = true; + this.checkScript(); + + this.focus(); + + await this.getController().setPreference("syntax-check", this.syntaxCheckEnabled); + } + + /** + * Disables checking for syntax errors + */ + async disableSyntaxCheck() { + this.syntaxCheckEnabled = false; + this.hideSyntaxErrors(); + + this.focus(); + + await this.getController().setPreference("syntax-check", this.syntaxCheckEnabled); + + // reset the timer... + if (this.timeout === null) + return; + + clearTimeout(this.timeout); + this.timeout = null; + } + + /** + * Checks if syntax checking is enabled + * @returns {boolean} + * true in case syntax check is enabled otherwise false + */ + isSyntaxCheckEnabled() { + return this.syntaxCheckEnabled; + } + + /** + * Shows a message box with the given syntax errors + * @param {string} errors + * the errors which should be displayed + */ + showSyntaxErrors(errors) { + const msg = document.querySelector("#sieve-editor-msg"); + msg.style.display = ''; + + const details = msg.querySelector(".sieve-editor-msg-details"); + while (details.firstChild) + details.firstChild.remove(); + + details.textContent = errors; + } + + /** + * Hides the syntax errors. + */ + hideSyntaxErrors() { + document.querySelector("#sieve-editor-msg").style.display = 'none'; + } + + /** + * Sets the editors indentation width. + * + * @param {int} width + * the indentation width in characters + * @returns {SieveEditorUI} + * a self reference + */ + async setIndentWidth(width) { + width = Number.parseInt(width, 10); + + if (Number.isNaN(width)) + throw new Error("Invalid Indent width"); + + this.cm.setOption("indentUnit", width); + await this.getController().setPreference("indentation-width", width); + + return this; + } + + /** + * Returns the indentation width. + * + * @returns {int} + * the indentation width in characters. + */ + getIndentWidth() { + return this.cm.getOption("indentUnit"); + } + + /** + * Sets the indent policy. + * + * @param {boolean} useTabs + * if true tabs are used for indenting otherwise spaces are used. + * @returns {SieveEditorUI} + * a self reference + */ + async setIndentWithTabs(useTabs) { + this.cm.setOption("indentWithTabs", useTabs); + + await this.getController().setPreference("indentation-policy", useTabs); + + return this; + } + + /** + * Returns the indent policy. + * + * @returns {boolean} + * true in case tabs are used to indent. False if spaces are used. + */ + getIndentWithTabs() { + return this.cm.getOption("indentWithTabs"); + } + + /** + * Sets the editor's tabulator width and persists the changed value. + * + * @param {int} tabSize + * the tabulator width in characters + * @returns {SieveEditorUI} + * a self reference + */ + async setTabWidth(tabSize) { + tabSize = Number.parseInt(tabSize, 10); + + if (Number.isNaN(tabSize)) + throw new Error(`Invalid Tab width ${tabSize}`); + + this.cm.setOption("tabSize", tabSize); + + await this.getController().setPreference("tabulator-width", tabSize); + + return this; + } + + /** + * Gets the editor's tabulator width. + * @returns {int} + * the tabulator width in characters. + */ + getTabWidth() { + return this.cm.getOption("tabSize"); + } + + /** + * @inheritdoc + */ + async loadSettings() { + const tabWidth = await this.getController().getPreference("tabulator-width"); + await this.setTabWidth(tabWidth); + + const IndentWithTabs = await this.getController().getPreference("indentation-policy"); + await this.setIndentWithTabs(IndentWithTabs); + + const indentWidth = await this.getController().getPreference("indentation-width"); + await this.setIndentWidth(indentWidth); + + const syntaxCheck = await this.getController().getPreference("syntax-check"); + if (syntaxCheck === false || syntaxCheck === "false") + await this.disableSyntaxCheck(); + else + await this.enableSyntaxCheck(); + } + + /** + * @inheritdoc + */ + async loadDefaultSettings() { + const tabWidth = await this.getController().getDefaultPreference("tabulator-width"); + await this.setTabWidth(tabWidth); + + const IndentWithTabs = await this.getController().getDefaultPreference("indentation-policy"); + await this.setIndentWithTabs(IndentWithTabs); + + const indentWidth = await this.getController().getDefaultPreference("indentation-width"); + await this.setIndentWidth(indentWidth); + + const syntaxCheck = await this.getController().getDefaultPreference("syntax-check"); + if (syntaxCheck === false) + await this.disableSyntaxCheck(); + else + await this.enableSyntaxCheck(); + + await this.renderSettings(); + } + + /** + * @inheritdoc + */ + async saveDefaultSettings() { + await this.getController().setDefaultPreference("tabulator-width", this.getTabWidth()); + + await this.getController().setDefaultPreference("indentation-policy", this.getIndentWithTabs()); + await this.getController().setDefaultPreference("indentation-width", this.getIndentWidth()); + + await this.getController().setDefaultPreference("syntax-check", this.isSyntaxCheckEnabled()); + } + +} + +export { SieveTextEditorUI }; diff --git a/src/common/managesieve.ui/editor/text/editor.plaintext.toolbar.html b/src/common/managesieve.ui/editor/text/editor.plaintext.toolbar.html index 6494f889..f8993369 100644 --- a/src/common/managesieve.ui/editor/text/editor.plaintext.toolbar.html +++ b/src/common/managesieve.ui/editor/text/editor.plaintext.toolbar.html @@ -1,7 +1,7 @@ <div> <div id="sieve-plaintext-editor-toolbar"> <div> - <div class="btn-group mr-2" role="group"> + <div class="btn-group me-2" role="group"> <button id="sieve-editor-cut" type="button" class="btn btn-sm btn-outline-secondary" data-i18n="texteditor.cut"></button> <button id="sieve-editor-copy" type="button" class="btn btn-sm btn-outline-secondary" @@ -10,19 +10,19 @@ data-i18n="texteditor.paste"></button> </div> - <div class="btn-group mr-2 " role="group"> + <div class="btn-group me-2 " role="group"> <button id="sieve-editor-undo" type="button" class="btn btn-sm btn-outline-secondary" data-i18n="texteditor.undo"></button> <button id="sieve-editor-redo" type="button" class="btn btn-sm btn-outline-secondary" data-i18n="texteditor.redo"></button> </div> - <div class="btn-group mr-2 " role="group"> + <div class="btn-group me-2 " role="group"> <button id="sieve-editor-replace-replace" type="button" class="btn btn-sm btn-outline-secondary" data-i18n="texteditor.findAndReplace"></button> </div> - <a class="btn-group mr-2 btn btn-sm btn-outline-secondary" + <a class="btn-group me-2 btn btn-sm btn-outline-secondary" href="https://thsmi.github.io/sieve-reference/en/index.html" data-i18n="texteditor.reference" target="_blank" role="button"></a> diff --git a/src/common/managesieve.ui/editor/text/editor.settings.indentation.tpl b/src/common/managesieve.ui/editor/text/editor.settings.indentation.html index d1dac04e..d1dac04e 100644 --- a/src/common/managesieve.ui/editor/text/editor.settings.indentation.tpl +++ b/src/common/managesieve.ui/editor/text/editor.settings.indentation.html diff --git a/src/common/managesieve.ui/editor/text/editor.settings.syntax.tpl b/src/common/managesieve.ui/editor/text/editor.settings.syntax.html index becb8fc9..becb8fc9 100644 --- a/src/common/managesieve.ui/editor/text/editor.settings.syntax.tpl +++ b/src/common/managesieve.ui/editor/text/editor.settings.syntax.html diff --git a/src/common/managesieve.ui/i18n/en-US.json b/src/common/managesieve.ui/i18n/en-US.json index 4b654731..dd3bfede 100644 --- a/src/common/managesieve.ui/i18n/en-US.json +++ b/src/common/managesieve.ui/i18n/en-US.json @@ -77,9 +77,9 @@ "credentials.sasl.default" : "Use suggested Mechanism", "credentials.sasl.plain" : "Force plain", "credentials.sasl.login" : "Force login (Deprecated)", - "credentials.sasl.crammd5" : "Force CRAM-MD5", "credentials.sasl.scramsha1" : "Force SCRAM-SHA-1", "credentials.sasl.scramsha256" : "Force SCRAM-SHA-256", + "credentials.sasl.scramsha512" : "Force SCRAM-SHA-512", "credentials.sasl.external" : "Force External", "credentials.sasl.none" : "No authentication", "credentials.username" : "Username", @@ -140,11 +140,9 @@ "authorization.accept" : "Authorize", "cert.title" : "Security alert", - "cert.description1" : "Your mail server's authenticity cannot be verified!", - "cert.description2" : "Someone might try to impersonate your mail server.", - "cert.error" : "The validation failed with the following error message:", - "cert.fingerprint" : "You need to verify manually, if the fingerprint matches your mailserver's fingerprint:", - "cert.warning" : "Continue only if the fingerprints match and the error message is reasonable!", + "cert.description1" : "Your mail server's authenticity could not be verified!", + "cert.warning" : "Continue only if the fingerprints match your mail server's fingerprints and the error message is reasonable.", + "cert.warning2" : "Someone might have impersonated your mail server!", "cert.accept" : "Continue", "script.create.title" : "Create Script", @@ -234,11 +232,15 @@ "debug.transport.states" : "Exceptions and State Machine Information", "debug.transport.rawdump" : "Raw Dump/Dump Byte Stream", "debug.transport.session" : "Session management", + "debug.transport.trace" : "Print full stack trace for log messages", "debug.global.title" : "Global", "debug.global.description" : "Used to log and debug the app's UI and rendering. The settings are global and apply to all accounts after restarting the app.", "debug.global.actions" : "User Events and Actions", "debug.global.ipc" : "IPC Messages", "debug.global.widgets" : "Widgets", - "debug.global.i18n" : "Internationalization (I18n)" -}
\ No newline at end of file + "debug.global.i18n" : "Internationalization (I18n)", + + "debug.console.show" : "Show Console", + "debug.ui.reload" : "Reload UI" +} diff --git a/src/common/managesieve.ui/settings/logic/SieveAbstractAccount.js b/src/common/managesieve.ui/settings/logic/SieveAbstractAccount.js deleted file mode 100644 index 6f102080..00000000 --- a/src/common/managesieve.ui/settings/logic/SieveAbstractAccount.js +++ /dev/null @@ -1,187 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - const { SievePrefManager } = require('./SievePrefManager.js'); - - const { SieveAuthorization } = require("./SieveAuthorizationSettings.js"); - const { SieveAuthentication } = require("./SieveAuthenticationSettings.js"); - const { SieveSecurity } = require("./SieveSecuritySettings.js"); - const { SieveHost } = require("./SieveHostSettings.js"); - const { SieveAccountSettings } = require("./SieveAccountSettings.js"); - const { SieveEditorSettings } = require("./SieveEditorSettings.js"); - - const { SieveLogger } = require("./../../utils/SieveLogger.js"); - - /** - * Abstract implementation for managing an account's preferences. - */ - class SieveAbstractAccount { - - /** - * Creates a new instance. - * - * @param {string} id - * the account's unique id. - */ - constructor(id) { - this.id = id; - - this.preferences = new SievePrefManager(`@${id}`); - - this.host = new SieveHost(this); - this.authentication = new SieveAuthentication(this); - this.authorization = new SieveAuthorization(this); - this.security = new SieveSecurity(this); - this.common = new SieveAccountSettings(this); - } - - /** - * Gets an instance to the logger. - * - * @returns {SieveLogger} - * an reference to the logger instance. - **/ - getLogger() { - return SieveLogger.getInstance(); - } - - /** - * Returns the account's unique id. - * @returns {string} - * the account unique id. - */ - getId() { - return this.id; - } - - /** - * Returns a reference to the preference manager for this account. - * - * @returns {SievePrefManager} - * the reference to the preference manager - */ - getConfig() { - return this.preferences; - } - - /** - * Contains information about the server settings like - * the hostname, port etc. - * - * @returns {SieveHost} - * the current host settings - **/ - async getHost() { - return await this.host.get(); - } - - /** - * Host alls security related setting like the SASL - * mechanisms or the tls configuration. - * - * @returns {SieveSecurity} - * the current security settings - */ - getSecurity() { - return this.security; - } - - /** - * Defines which authentication configuration is active. - * - * An account may support multiple concurrent configurations - * from which only one can be active. - * - * E.g using Thunderbird's account settings - * on user specified settings. - * - * @param {int} type - * the authentication type which should be activated. - * - * @returns {SieveAbstractAccount} - * a self reference - */ - async setAuthentication(type) { - await this.authentication.setMechanism(type); - return this; - } - - /** - * Gets the authentication configuration - * - * @param {int} [type] - * optional the configuration type. If omitted the default type is returned. - * @returns {SieveAbstractAuthentication} - * the object managing the authentication for the type. - */ - async getAuthentication(type) { - return await this.authentication.get(type); - } - - /** - * Defines which authorization configuration is active. - * - * @param {int} type - * the authorization type which should be activated. - * @returns {SieveAbstractAccount} - * a self reference - */ - async setAuthorization(type) { - await this.authorization.setMechanism(type); - return this; - } - - /** - * Gets the authorization configuration. - * - * @param {SieveAbstractAuthorization} [type] - * optional authorization type. In case it is omitted - * default settings are returned. - * @returns {SieveAbstractAuthorization} - * the object managing the authorization for the type - */ - async getAuthorization(type) { - return await this.authorization.get(type); - } - - /** - * Gets miscellaneous account specific settings like the log levels etc. - * - * @returns {SieveAccountSettings} - * the object managing the miscellaneous account settings - */ - getSettings() { - return this.common; - } - - /** - * Gets the object managing the accounts editor's settings. - * - * @returns {SieveEditorSettings} - * the settings object - */ - getEditor() { - return new SieveEditorSettings( - new SievePrefManager(this.getConfig().getNamespace())); - } - } - - // Require modules need to use export.module - if (typeof (module) !== "undefined" && module && module.exports) - module.exports.SieveAbstractAccount = SieveAbstractAccount; - else - exports.SieveAbstractAccount = SieveAbstractAccount; - -})(this); diff --git a/src/common/managesieve.ui/settings/logic/SieveAbstractAccount.mjs b/src/common/managesieve.ui/settings/logic/SieveAbstractAccount.mjs new file mode 100644 index 00000000..4b7073c7 --- /dev/null +++ b/src/common/managesieve.ui/settings/logic/SieveAbstractAccount.mjs @@ -0,0 +1,154 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +import { SievePrefManager } from "./SievePrefManager.mjs"; + +import { SieveAuthorization } from "./SieveAuthorization.mjs"; +import { SieveAuthentication } from "./SieveAuthentication.mjs"; +import { SieveSecurity } from "./SieveSecurity.mjs"; +import { SieveHost } from "./SieveHost.mjs"; +import { SieveAccountSettings } from "./SieveAccountSettings.mjs"; +import { SieveEditorSettings } from "./SieveEditorSettings.mjs"; + +import { SieveLogger } from "./../../utils/SieveLogger.mjs"; +/** + * Abstract implementation for managing an account's preferences. + */ +class SieveAbstractAccount { + + /** + * Creates a new instance. + * + * @param {string} id + * the account's unique id. + */ + constructor(id) { + this.id = id; + + this.preferences = new SievePrefManager(`@${id}`); + + this.host = new SieveHost(this); + this.authentication = new SieveAuthentication(this); + this.authorization = new SieveAuthorization(this); + this.security = new SieveSecurity(this); + this.common = new SieveAccountSettings(this); + } + + /** + * Gets an instance to the logger. + * + * @returns {SieveLogger} + * an reference to the logger instance. + **/ + getLogger() { + return SieveLogger.getInstance(); + } + + /** + * Returns the account's unique id. + * @returns {string} + * the account unique id. + */ + getId() { + return this.id; + } + + /** + * Returns a reference to the preference manager for this account. + * + * @returns {SievePrefManager} + * the reference to the preference manager + */ + getConfig() { + return this.preferences; + } + + /** + * Contains information about the server settings like + * the hostname, port etc. + * + * @returns {SieveHost} + * the current host settings + **/ + getHost() { + return this.host; + } + + /** + * Host alls security related setting like the SASL + * mechanisms or the tls configuration. + * + * @returns {SieveSecurity} + * the current security settings + */ + getSecurity() { + return this.security; + } + + /** + * Gets the authentication configuration + * + * @returns {SieveAbstractAuthentication} + * the object managing the authentication for the type. + */ + getAuthentication() { + return this.authentication; + } + + /** + * Defines which authorization configuration is active. + * + * @param {int} type + * the authorization type which should be activated. + * @returns {SieveAbstractAccount} + * a self reference + */ + async setAuthorization(type) { + await this.authorization.setMechanism(type); + return this; + } + + /** + * Gets the authorization configuration. + * + * @param {SieveAbstractAuthorization} [type] + * optional authorization type. In case it is omitted + * default settings are returned. + * @returns {SieveAbstractAuthorization} + * the object managing the authorization for the type + */ + async getAuthorization(type) { + return await this.authorization.get(type); + } + + /** + * Gets miscellaneous account specific settings like the log levels etc. + * + * @returns {SieveAccountSettings} + * the object managing the miscellaneous account settings + */ + getSettings() { + return this.common; + } + + /** + * Gets the object managing the accounts editor's settings. + * + * @returns {SieveEditorSettings} + * the settings object + */ + getEditor() { + return new SieveEditorSettings( + new SievePrefManager(this.getConfig().getNamespace())); + } +} + +export { SieveAbstractAccount }; diff --git a/src/common/managesieve.ui/settings/logic/SieveAbstractAccounts.js b/src/common/managesieve.ui/settings/logic/SieveAbstractAccounts.js deleted file mode 100644 index 943709dd..00000000 --- a/src/common/managesieve.ui/settings/logic/SieveAbstractAccounts.js +++ /dev/null @@ -1,125 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - - -(function (exports) { - - "use strict"; - - const CONFIG_ID_GLOBAL = "global"; - const CONFIG_KEY_LOG_LEVEL = "loglevel"; - - const DEFAULT_LOG_LEVEL = 0; - - const { SieveUniqueId } = require("./../../utils/SieveUniqueId.js"); - const { SievePrefManager } = require('./SievePrefManager.js'); - const { SieveEditorSettings } = require("./SieveEditorSettings.js"); - - /** - * Abstract class which manages sieve accounts. - */ - class SieveAbstractAccounts { - - /** - * Creates a new instance - */ - constructor() { - this.accounts = {}; - } - - /** - * Loads the list of accounts configurations. - * @abstract - * - * @returns {SieveAccounts} - * a self reference. - */ - // eslint-disable-next-line require-await - async load() { - throw new Error("Implement me"); - } - - /** - * Generates a pseudo unique id. - * The id is guaranteed to be made of alphanumerical characters and dashes. - * - * @returns {string} - * the unique id in string representation. - */ - generateId() { - return (new SieveUniqueId()).generate(); - } - - /** - * Returns a list with all accounts. - * The accounts are returned as key value pairs (unique id and Account) - * - * @returns { object<string, SieveAccount>} - * a list with sieve account. - */ - getAccountIds() { - return Object.keys(this.accounts); - } - - /** - * Returns a specific sieve account - * @param {string} id - * the accounts unique id. - * @returns {SieveAccount} - * the sieve account or undefined. - */ - getAccountById(id) { - return this.accounts[id]; - } - - /** - * Sets the global log level. - * - * @param {int} level - * the global log level as integer. - * @returns {SieveAccounts} - * a self reference. - */ - async setLogLevel(level) { - await (new SievePrefManager(CONFIG_ID_GLOBAL)).setInteger(CONFIG_KEY_LOG_LEVEL, level); - return this; - } - - /** - * Gets the global log level. - * - * @returns {int} - * the log level as integer. - */ - async getLogLevel() { - return await (new SievePrefManager(CONFIG_ID_GLOBAL)) - .getInteger(CONFIG_KEY_LOG_LEVEL, DEFAULT_LOG_LEVEL); - } - - /** - * Gets the object managing the editor's default settings. - * - * @returns {SieveEditorSettings} - * the settings object - */ - getEditor() { - return new SieveEditorSettings(new SievePrefManager("defaults")); - } - - } - - // Require modules need to use export.module - if (typeof (module) !== "undefined" && module && module.exports) - module.exports.SieveAbstractAccounts = SieveAbstractAccounts; - else - exports.SieveAbstractAccounts = SieveAbstractAccounts; - -})(this); diff --git a/src/common/managesieve.ui/settings/logic/SieveAbstractAccounts.mjs b/src/common/managesieve.ui/settings/logic/SieveAbstractAccounts.mjs new file mode 100644 index 00000000..a9a51b92 --- /dev/null +++ b/src/common/managesieve.ui/settings/logic/SieveAbstractAccounts.mjs @@ -0,0 +1,114 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + + +const CONFIG_ID_GLOBAL = "global"; +const CONFIG_KEY_LOG_LEVEL = "loglevel"; + +const DEFAULT_LOG_LEVEL = 0; + +import { SieveUniqueId } from "./../../utils/SieveUniqueId.mjs"; +import { SievePrefManager } from "./SievePrefManager.mjs"; +import { SieveEditorSettings } from "./SieveEditorSettings.mjs"; + +/** + * Abstract class which manages sieve accounts. + */ +class SieveAbstractAccounts { + + /** + * Creates a new instance + */ + constructor() { + this.accounts = {}; + } + + /** + * Loads the list of accounts configurations. + * @abstract + * + * @returns {SieveAccounts} + * a self reference. + */ + async load() { + throw new Error("Implement me"); + } + + /** + * Generates a pseudo unique id. + * The id is guaranteed to be made of alphanumerical characters and dashes. + * + * @returns {string} + * the unique id in string representation. + */ + generateId() { + return (new SieveUniqueId()).generate(); + } + + /** + * Returns a list with all accounts. + * The accounts are returned as key value pairs (unique id and Account) + * + * @returns { object<string, SieveAccount>} + * a list with sieve account. + */ + getAccountIds() { + return Object.keys(this.accounts); + } + + /** + * Returns a specific sieve account + * @param {string} id + * the accounts unique id. + * @returns {SieveAccount} + * the sieve account or undefined. + */ + getAccountById(id) { + return this.accounts[id]; + } + + /** + * Sets the global log level. + * + * @param {int} level + * the global log level as integer. + * @returns {SieveAccounts} + * a self reference. + */ + async setLogLevel(level) { + await (new SievePrefManager(CONFIG_ID_GLOBAL)).setInteger(CONFIG_KEY_LOG_LEVEL, level); + return this; + } + + /** + * Gets the global log level. + * + * @returns {int} + * the log level as integer. + */ + async getLogLevel() { + return await (new SievePrefManager(CONFIG_ID_GLOBAL)) + .getInteger(CONFIG_KEY_LOG_LEVEL, DEFAULT_LOG_LEVEL); + } + + /** + * Gets the object managing the editor's default settings. + * + * @returns {SieveEditorSettings} + * the settings object + */ + getEditor() { + return new SieveEditorSettings(new SievePrefManager("defaults")); + } + +} + +export { SieveAbstractAccounts }; diff --git a/src/common/managesieve.ui/settings/logic/SieveAbstractAuthentication.js b/src/common/managesieve.ui/settings/logic/SieveAbstractAuthentication.js deleted file mode 100644 index 63134b12..00000000 --- a/src/common/managesieve.ui/settings/logic/SieveAbstractAuthentication.js +++ /dev/null @@ -1,79 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - /** - * An base class common for all authentication mechanisms - **/ - class SieveAbstractAuthentication { - - /** - * Create a new instance. - * - * @param {int} type - * the accounts unique identifier. - * @param {SieveAccount} account - * a reference to the parent sieve account. - */ - constructor(type, account) { - this.type = type; - this.account = account; - } - - /** - * Returns the password for the chosen mechanism. - * - * Not all mechanisms require a password. - * Others e.g. when prompting do not always return a password. - * - * @abstract - * - * @returns {string | null} - * the password or null e.g. when the password prompt was dismissed. - **/ - // eslint-disable-next-line require-await - async getPassword() { - throw new Error("Implement getPassword"); - } - - /** - * Returns the username for the account. - * - * @abstract - * - * @returns {string} - * the username as string. - **/ - // eslint-disable-next-line require-await - async getUsername() { - throw new Error("Implement getUsername"); - } - - /** - * Each authentication type has an unique identifier. - * - * @returns {int} - * the identifier as int. - */ - getType() { - return this.type; - } - } - - if (typeof (module) !== "undefined" && module && module.exports) - module.exports.SieveAbstractAuthentication = SieveAbstractAuthentication; - else - exports.SieveAbstractAuthentication = SieveAbstractAuthentication; - -})(this); diff --git a/src/common/managesieve.ui/settings/logic/SieveAbstractAuthentication.mjs b/src/common/managesieve.ui/settings/logic/SieveAbstractAuthentication.mjs new file mode 100644 index 00000000..1755cb1b --- /dev/null +++ b/src/common/managesieve.ui/settings/logic/SieveAbstractAuthentication.mjs @@ -0,0 +1,55 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +/** + * An base class common for all authentication mechanisms + **/ +class SieveAbstractAuthentication { + + /** + * Create a new instance. + * + * @param {SieveAccount} account + * a reference to the parent sieve account. + */ + constructor(account) { + this.account = account; + } + + /** + * Returns the password for the chosen mechanism. + * + * Not all mechanisms require a password. + * Others e.g. when prompting do not always return a password. + * + * @abstract + * + * @returns {string | null} + * the password or null e.g. when the password prompt was dismissed. + **/ + async getPassword() { + throw new Error("Implement getPassword()"); + } + + /** + * Returns the username for the account. + * + * @abstract + * + * @returns {string} + * the username as string. + **/ + async getUsername() { + throw new Error("Implement getUsername()"); + } +} + +export { SieveAbstractAuthentication }; diff --git a/src/common/managesieve.ui/settings/logic/SieveAbstractAuthorization.js b/src/common/managesieve.ui/settings/logic/SieveAbstractAuthorization.js deleted file mode 100644 index a8807524..00000000 --- a/src/common/managesieve.ui/settings/logic/SieveAbstractAuthorization.js +++ /dev/null @@ -1,136 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - const CONFIG_AUTHORIZATION_USERNAME = "sasl.authorization.username"; - const DEFAULT_AUTHORIZATION = ""; - - /** - * An base class common for all authorization mechanism - **/ - class SieveAbstractAuthorization { - - /** - * Create a new instance. - * - * @param {int} type - * the account's unique identifier. - * @param {SieveAccount} account - * a reference to the parent sieve account. - */ - constructor(type, account) { - this.account = account; - this.type = type; - } - - /** - * Each authorization type has an unique identifier. - * - * @returns {int} - * the identifier as int. - */ - getType() { - return this.type; - } - - /** - * Returns the authorization string. - * - * @abstract - * - * @returns {string} - * the authorization string - */ - getAuthorization() { - throw new Error("Implement SieveAbstractAuthorizationMechanism::getAuthorization"); - } - } - - /** - * The easiest authorization mechanism. - * When an empty string is passed the server should choose the most suitable authorization. - */ - class SieveNoAuthorization extends SieveAbstractAuthorization { - - /** - * Returns always an empty string. This means the server should choose the most suitable authorization. - * - * @returns {string} - * an empty string. - */ - getAuthorization() { - return DEFAULT_AUTHORIZATION; - } - } - - /** - * Uses for authorization the same username which was used for authentication. - */ - class SieveDefaultAuthorization extends SieveAbstractAuthorization { - - /** - * Returns the username which was used for authentication. - * - * @returns {string} - * the username as string. - */ - async getAuthorization() { - return await (await this.account.getAuthentication()).getUsername(); - } - } - - /** - * Uses a custom authorization. - */ - class SieveCustomAuthorization extends SieveAbstractAuthorization { - - /** - * @inheritdoc - **/ - async getAuthorization() { - return await this.account.getConfig().getString(CONFIG_AUTHORIZATION_USERNAME, null); - } - - /** - * Sets a custom authorization. - * - * @param {string} authorization - * the authorization as string. - * - */ - async setAuthorization(authorization) { - if (typeof (authorization) === "undefined" || (authorization === null)) - throw new Error("Authorization can't be undefined"); - - await this.account.getConfig().setString(CONFIG_AUTHORIZATION_USERNAME, authorization); - } - } - - // Require modules need to use export.module - if (typeof (module) !== "undefined" && module && module.exports) { - module.exports.SieveAbstractAuthorization = SieveAbstractAuthorization; - - module.exports.SieveNoAuthorization = SieveNoAuthorization; - module.exports.SieveCustomAuthorization = SieveCustomAuthorization; - module.exports.SieveDefaultAuthorization = SieveDefaultAuthorization; - } else { - exports.SieveAbstractAuthorization = SieveAbstractAuthorization; - - exports.SieveNoAuthorization = SieveNoAuthorization; - exports.SieveCustomAuthorization = SieveCustomAuthorization; - exports.SieveDefaultAuthorization = SieveDefaultAuthorization; - } - - -})(this); diff --git a/src/common/managesieve.ui/settings/logic/SieveAbstractAuthorization.mjs b/src/common/managesieve.ui/settings/logic/SieveAbstractAuthorization.mjs new file mode 100644 index 00000000..26d7aa62 --- /dev/null +++ b/src/common/managesieve.ui/settings/logic/SieveAbstractAuthorization.mjs @@ -0,0 +1,121 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +const CONFIG_AUTHORIZATION_USERNAME = "sasl.authorization.username"; +const DEFAULT_AUTHORIZATION = ""; + +/** + * An base class common for all authorization mechanism + **/ +class SieveAbstractAuthorization { + + /** + * Create a new instance. + * + * @param {int} type + * the account's unique identifier. + * @param {SieveAccount} account + * a reference to the parent sieve account. + */ + constructor(type, account) { + this.account = account; + this.type = type; + } + + /** + * Each authorization type has an unique identifier. + * + * @returns {int} + * the identifier as int. + */ + getType() { + return this.type; + } + + /** + * Returns the authorization string. + * + * @abstract + * + * @returns {string} + * the authorization string + */ + getAuthorization() { + throw new Error("Implement SieveAbstractAuthorizationMechanism::getAuthorization"); + } +} + +/** + * The easiest authorization mechanism. + * When an empty string is passed the server should choose the most suitable authorization. + */ +class SieveNoAuthorization extends SieveAbstractAuthorization { + + /** + * Returns always an empty string. This means the server should choose the most suitable authorization. + * + * @returns {string} + * an empty string. + */ + getAuthorization() { + return DEFAULT_AUTHORIZATION; + } +} + +/** + * Uses for authorization the same username which was used for authentication. + */ +class SieveDefaultAuthorization extends SieveAbstractAuthorization { + + /** + * Returns the username which was used for authentication. + * + * @returns {string} + * the username as string. + */ + async getAuthorization() { + return await (await this.account.getAuthentication()).getUsername(); + } +} + +/** + * Uses a custom authorization. + */ +class SieveCustomAuthorization extends SieveAbstractAuthorization { + + /** + * @inheritdoc + **/ + async getAuthorization() { + return await this.account.getConfig().getString(CONFIG_AUTHORIZATION_USERNAME, null); + } + + /** + * Sets a custom authorization. + * + * @param {string} authorization + * the authorization as string. + * + */ + async setAuthorization(authorization) { + if (typeof (authorization) === "undefined" || (authorization === null)) + throw new Error("Authorization can't be undefined"); + + await this.account.getConfig().setString(CONFIG_AUTHORIZATION_USERNAME, authorization); + } +} + +export { + SieveAbstractAuthorization, + SieveNoAuthorization, + SieveCustomAuthorization, + SieveDefaultAuthorization +}; diff --git a/src/common/managesieve.ui/settings/logic/SieveAbstractHost.js b/src/common/managesieve.ui/settings/logic/SieveAbstractHost.js deleted file mode 100644 index d3962300..00000000 --- a/src/common/managesieve.ui/settings/logic/SieveAbstractHost.js +++ /dev/null @@ -1,193 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - const PORT_SIEVE_RFC = 4190; - const PORT_SIEVE_OLD = 2000; - - const TYPE_RFC = 0; - const TYPE_OLD = 1; - const TYPE_CUSTOM = 2; - - const CONFIG_HOST_PORT_TYPE = "port.type"; - const CONFIG_HOST_PORT = "port"; - - const CONFIG_KEEP_ALIVE_INTERVAL = "keepalive"; - - // eslint-disable-next-line no-magic-numbers - const ONE_MINUTE = 60 * 1000; - // eslint-disable-next-line no-magic-numbers - const FIVE_MINUTES = 5 * ONE_MINUTE; - - - /** - * An abstract implementation for the host settings. - * They define the hostname as well as the port. - **/ - class SieveAbstractHost { - - /** - * Creates a new instance. - * - * @param {int} type - * the accounts unique identifier. - * @param {SieveAccount} account - * a reference to the parent sieve account. - */ - constructor(type, account) { - this.account = account; - this.type = type; - } - - /** - * Gets the hostname for this account - * @abstract - * @returns {string} - * the hostname as string. - */ - getHostname() { - throw new Error("Implement getHostname"); - } - - /** - * Returns the port for this configuration. - * - * @param {int} [type] - * Use zero to get the standard port 4190. - * One returns the old port 2000. - * And two the currently configured port. - * if omitted the port for the current type is returned - * - * @returns {int} - * the port as integer for this account. - */ - async getPort(type) { - - if (typeof (type) === "undefined" || type === null) - type = await this.account.getConfig().getInteger(CONFIG_HOST_PORT_TYPE, TYPE_RFC); - - if (type === TYPE_CUSTOM) - return await this.account.getConfig().getInteger(CONFIG_HOST_PORT, PORT_SIEVE_RFC); - - if (type === TYPE_OLD) - return PORT_SIEVE_OLD; - - return PORT_SIEVE_RFC; - } - - /** - * Configures the TCP Port which sieve should use. - * - * @param {string} port - * the port number as string. - * - * @returns {SieveAbstractHost} - * a self reference - */ - async setPort(port) { - let type = TYPE_CUSTOM; - - if (port === PORT_SIEVE_RFC) - type = TYPE_RFC; - else if (port === PORT_SIEVE_OLD) - type = TYPE_OLD; - - await this.account.getConfig().setInteger(CONFIG_HOST_PORT_TYPE, type); - - if (type !== TYPE_CUSTOM) - return this; - - port = parseInt(port, 10); - - if (isNaN(port)) - port = PORT_SIEVE_RFC; - - await this.account.getConfig().setInteger(CONFIG_HOST_PORT, port); - return this; - } - - /** - * Each host type has an unique identifier. - * - * @returns {int} - * the identifier as int. - */ - getType() { - return this.type; - } - - /** - * Configures the maximum idle time after a message is send. - * In case the time span elapsed an keep alive message will be - * send to the server. - * - * @param {int} value - * the maximal time in seconds. zero disables keep alive messages - * - * @returns {SieveAbstractHost} - * a self reference - */ - async setKeepAlive(value) { - await this.account.getConfig().setInteger(CONFIG_KEEP_ALIVE_INTERVAL, value); - return this; - } - - /** - * Gets the maximum idle time after a message is send - * @returns {int} - * the maximum idle time in seconds. - * zero indicates keep alive messages are disabled - **/ - async getKeepAlive() { - return await this.account.getConfig().getInteger(CONFIG_KEEP_ALIVE_INTERVAL, FIVE_MINUTES); - } - } - - /** - * This Class manages a custom host setting for a Sieve Account. Sieve Accounts - * are identified by URIs. - */ - class SieveCustomHost extends SieveAbstractHost { - - /** - * @inheritdoc - **/ - async getHostname() { - return await this.account.getConfig().getString("hostname", ""); - } - - /** - * Sets the custom hostname which shall be used. - * - * @param {string} hostname - * the hostname or ip as string. - * - * @returns {SieveCustomHost} - * a self reference - */ - async setHostname(hostname) { - await this.account.getConfig().setString("hostname", hostname); - return this; - } - } - - if (typeof (module) !== "undefined" && module && module.exports) { - module.exports.SieveCustomHost = SieveCustomHost; - module.exports.SieveAbstractHost = SieveAbstractHost; - } else { - exports.SieveCustomHost = SieveCustomHost; - exports.SieveAbstractHost = SieveAbstractHost; - } - -})(this); diff --git a/src/common/managesieve.ui/settings/logic/SieveAbstractHost.mjs b/src/common/managesieve.ui/settings/logic/SieveAbstractHost.mjs new file mode 100644 index 00000000..4dacb44c --- /dev/null +++ b/src/common/managesieve.ui/settings/logic/SieveAbstractHost.mjs @@ -0,0 +1,107 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +const PORT_SIEVE_RFC = 4190; +const CONFIG_HOST_PORT = "port"; + + + +/** + * An abstract implementation for the host settings. + * They define the hostname as well as the port. + **/ +class SieveAbstractHost { + + /** + * Gets the hostname for this account + * @abstract + * + * @returns {string} + * the hostname as string. + */ + async getHostname() { + throw new Error("Implement getHostname"); + } + + /** + * Returns the port for this configuration. + * @abstract + * + * @returns {int} + * the port as integer for this account. + */ + async getPort() { + throw new Error("Implement getPort()"); + } + + /** + * Gets the maximum idle time after a message is send + * @abstract + * + * @returns {int} + * the maximum idle time in seconds. + * zero indicates keep alive messages are disabled + **/ + async getKeepAlive() { + throw new Error("Implement getKeepAlive()"); + } +} + +/** + * This Class manages a custom host setting for a Sieve Account. Sieve Accounts + * are identified by URIs. + */ +class SieveCustomHost extends SieveAbstractHost { + + /** + * Creates a new instance. + * + * @param {SieveAccount} account + * a reference to the parent sieve account. + */ + constructor(account) { + super(); + this.account = account; + } + + + /** + * @inheritdoc + */ + async getPort() { + return await (this.account.getConfig().getInteger(CONFIG_HOST_PORT, PORT_SIEVE_RFC)); + } + + /** + * Configures the TCP Port which sieve should use. + * + * @param {string} port + * the port number as string. In case it is no number or an invalid + * number it will silently fall back to the default port + * + * @returns {SieveAbstractHost} + * a self reference + */ + async setPort(port) { + port = Number.parseInt(port, 10); + + if (Number.isNaN(port)) + port = PORT_SIEVE_RFC; + + await this.account.getConfig().setInteger(CONFIG_HOST_PORT, port); + return this; + } +} + +export { + SieveCustomHost, + SieveAbstractHost +}; diff --git a/src/common/managesieve.ui/settings/logic/SieveAbstractMechanism.js b/src/common/managesieve.ui/settings/logic/SieveAbstractMechanism.js deleted file mode 100644 index f4ed90ce..00000000 --- a/src/common/managesieve.ui/settings/logic/SieveAbstractMechanism.js +++ /dev/null @@ -1,134 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - /** - * A union for a group of similar mechanisms. - */ - class SieveAbstractMechanism { - - /** - * Creates a new instance. - * - * @param {SieveAccount} account - * a reference to the parent sieve account. - */ - constructor(account) { - this.account = account; - } - - /** - * The pref key which is used to store the information about the mechanism. - * @abstract - * - * @returns {string} - * the pref key - **/ - getKey() { - throw new Error("Implement getKey()"); - } - - /** - * Returns the default mechanism which is used unless it is - * overwritten by an other mechanism. - * @abstract - * - * @returns {int} - * the default mechanism - */ - getDefault() { - throw new Error("Implement getDefault()"); - } - - /** - * Checks it the given mechanism is supported. - * @abstract - * - * @param {int} mechanism - * the authentication mechanisms unique id. - * @returns {boolean} - * true in case the given type is supported otherwise false. - */ - hasMechanism(mechanism) { - throw new Error(`Implement hasMechanism(${mechanism})`); - } - - /** - * Returns the authentication mechanism for the given type. - * @abstract - * - * @param {int} mechanism - * the authentication mechanism - * @returns {SieveAbstractAuthentication} - * the authentication mechanism instance. - */ - getMechanismById(mechanism) { - throw new Error(`Implement getMechanism(${mechanism})`); - } - - /** - * Sets the current mechanism. - * - * @param {int} type - * the mechanism type's unique id. - */ - async setMechanism(type) { - if (typeof (type) === "undefined" || type === null) - type = this.getDefault(); - - if (typeof (type) === "string") - type = Number.parseInt(type, 10); - - if (!this.hasMechanism(type)) - throw new Error(`Invalid mechanism ${type}`); - - await this.account.getConfig().setInteger(this.getKey(), type); - } - - /** - * Gets the current mechanism. - * - * @returns {int} - * the mechanisms unique id. - */ - async getMechanism() { - return await this.account.getConfig().getInteger(this.getKey(), this.getDefault()); - } - - /** - * Returns the settings for the given authentication mechanism - * - * @param {int} [mechanism] - * the optional authentication mechanism's unique id. - * If omitted the currently active authentication is returned. - * @returns {SieveAbstractAuthentication} - * the selected authentication mechanism. - */ - async get(mechanism) { - if (typeof (mechanism) === "undefined" || mechanism === null) - mechanism = await this.getMechanism(); - - if (!this.hasMechanism(mechanism)) - mechanism = this.getDefault(); - - return await this.getMechanismById(mechanism); - } - } - - if (typeof (module) !== "undefined" && module && module.exports) - module.exports.SieveAbstractMechanism = SieveAbstractMechanism; - else - exports.SieveAbstractMechanism = SieveAbstractMechanism; - -})(this); diff --git a/src/common/managesieve.ui/settings/logic/SieveAbstractMechanism.mjs b/src/common/managesieve.ui/settings/logic/SieveAbstractMechanism.mjs new file mode 100644 index 00000000..7d8192d9 --- /dev/null +++ b/src/common/managesieve.ui/settings/logic/SieveAbstractMechanism.mjs @@ -0,0 +1,125 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +/** + * A union for a group of similar mechanisms. + */ +class SieveAbstractMechanism { + + /** + * Creates a new instance. + * + * @param {SieveAccount} account + * a reference to the parent sieve account. + */ + constructor(account) { + this.account = account; + } + + /** + * The pref key which is used to store the information about the mechanism. + * @abstract + * + * @returns {string} + * the pref key + **/ + getKey() { + throw new Error("Implement getKey()"); + } + + /** + * Returns the default mechanism which is used unless it is + * overwritten by an other mechanism. + * @abstract + * + * @returns {int} + * the default mechanism + */ + getDefault() { + throw new Error("Implement getDefault()"); + } + + /** + * Checks it the given mechanism is supported. + * @abstract + * + * @param {int} mechanism + * the authentication mechanisms unique id. + * @returns {boolean} + * true in case the given type is supported otherwise false. + */ + hasMechanism(mechanism) { + throw new Error(`Implement hasMechanism(${mechanism})`); + } + + /** + * Returns the authentication mechanism for the given type. + * @abstract + * + * @param {int} mechanism + * the authentication mechanism + * @returns {SieveAbstractAuthentication} + * the authentication mechanism instance. + */ + getMechanismById(mechanism) { + throw new Error(`Implement getMechanism(${mechanism})`); + } + + /** + * Sets the current mechanism. + * + * @param {int} type + * the mechanism type's unique id. + */ + async setMechanism(type) { + if (typeof (type) === "undefined" || type === null) + type = this.getDefault(); + + if (typeof (type) === "string") + type = Number.parseInt(type, 10); + + if (!this.hasMechanism(type)) + throw new Error(`Invalid mechanism ${type}`); + + await this.account.getConfig().setInteger(this.getKey(), type); + } + + /** + * Gets the current mechanism. + * + * @returns {int} + * the mechanisms unique id. + */ + async getMechanism() { + return await this.account.getConfig().getInteger(this.getKey(), this.getDefault()); + } + + /** + * Returns the settings for the given authentication mechanism + * + * @param {int} [mechanism] + * the optional authentication mechanism's unique id. + * If omitted the currently active authentication is returned. + * @returns {SieveAbstractAuthentication} + * the selected authentication mechanism. + */ + async get(mechanism) { + if (typeof (mechanism) === "undefined" || mechanism === null) + mechanism = await this.getMechanism(); + + if (!this.hasMechanism(mechanism)) + mechanism = this.getDefault(); + + return await this.getMechanismById(mechanism); + } +} + +export { SieveAbstractMechanism }; diff --git a/src/common/managesieve.ui/settings/logic/SieveAbstractPrefManager.js b/src/common/managesieve.ui/settings/logic/SieveAbstractPrefManager.js deleted file mode 100644 index 5fa7fc02..00000000 --- a/src/common/managesieve.ui/settings/logic/SieveAbstractPrefManager.js +++ /dev/null @@ -1,245 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - /** - * Manages preferences. - */ - class SieveAbstractPrefManager { - - /** - * Initializes the preference manager. - * - * @param {string} [namespace] - * the optional preferences namespace. - */ - constructor(namespace) { - if ((typeof(namespace) === "undefined") || (namespace === null)) - namespace = ""; - - this.namespace = namespace; - } - - /** - * Returns the namespace which is added to all preferences - * - * @returns {string} - * the prefix as string. - */ - getNamespace() { - return this.namespace; - } - - /** - * Returns a specific value. - * @abstract - * - * @param {string} key - * the key which should be returned. - * @returns {object} - * the value or undefined in case it does not exist. - */ - // eslint-disable-next-line require-await - async getValue(key) { - throw new Error(`Implement SieveAbstractPrefManager::getValue(${key})`); - } - - /** - * Sets and persists the given preference. - * @abstract - * - * @param {string} key - * the preference key which should be written. - * @param {object} value - * the key's value. - * @returns {SievePrefManager} - * a self reference. - */ - // eslint-disable-next-line require-await - async setValue(key, value) { - throw new Error(`Implement SieveAbstractPrefManager::setValue(${key},${value})`); - } - - /** - * Returns the boolean value for the preference. - * - * @param {string} key - * the preference's key - * @param {boolean} [fallback] - * the fallback value in case the key does not exist. - * @returns {boolean} - * the key's value as boolean - */ - async getBoolean(key, fallback) { - const value = await this.getValue(key); - - if (typeof (value) === "undefined" || value === null) - return fallback; - - // Parse boolean - if ((value === true) || (value === "true")) - return true; - - if ((value === false) || (value === "false")) - return false; - - return fallback; - } - - /** - * Sets a boolean value for the given key. - * - * @param {string} key - * the preference's key - * @param {boolean} value - * the value which should be set - * @returns {SievePrefManager} - * a self reference - */ - async setBoolean(key, value) { - // ensure it is an boolean... - value = !!value; - - await this.setValue(key, value); - return this; - } - - /** - * Returns the string value for the preference. - * - * @param {string} key - * the preference key - * @param {string} [fallback] - * the fallback value in case the key does not exist. - * @returns {string} - * the key's value as string - */ - async getString(key, fallback) { - const value = await this.getValue(key); - - if (typeof (value) === "undefined" || value === null) - return fallback; - - return `${value}`; - } - - /** - * The string which should be set for th preference - * - * @param {string} key - * the preference key - * @param {string} value - * the key's value as string - * @returns {SievePrefManager} - * a self reference - */ - async setString(key, value) { - await this.setValue(key, `${value}`); - return this; - } - - /** - * Returns the integer value for the preference. - * - * @param {string} key - * the preference's key - * @param {int} [fallback] - * the fallback value in case the key does not exist or is not a number. - * @returns {string} - * the key's value as integer - */ - async getInteger(key, fallback) { - - let value = await this.getValue(key); - - if (typeof (value) === "undefined" || value === null || Number.isNaN(value)) - return fallback; - - if (Number.isInteger(value)) - return value; - - try { - value = Number.parseInt(value, 10); - } catch (ex) { - return fallback; - } - - if (Number.isNaN(value)) - return fallback; - - return value; - } - - /** - * Sets an integer value for the given key. - * - * @param {string} key - * the preference's key - * @param {int} value - * the integer value which should be set. - * @returns {SievePrefManager} - * a self reference. - */ - async setInteger(key, value) { - await this.setValue(key, Number.parseInt(value, 10)); - return this; - } - - /** - * Saves a complex value like an object for the given key. - * The object needs to be serializable to a json string. - * - * @param {string} key - * the preference's key - * @param {object} value - * the complex value which should be saved. - * @returns {SievePrefManager} - * a self reference. - */ - async setComplexValue(key, value) { - await this.setValue(key, JSON.stringify(value)); - return this; - } - - /** - * Returns the complex value for the given key. - * - * @param {string} key - * the preference's key. - * @param {object} fallback - * the fallback value in case the key does not exist. - * @returns {object} - * the key's complex value. - */ - async getComplexValue(key, fallback) { - const value = await this.getValue(key); - - if (typeof (value) === "undefined" || value === null) - return fallback; - - try { - return JSON.parse(value); - } catch (ex) { - return fallback; - } - } - - } - - if (typeof(module) !== "undefined" && module && module.exports) - module.exports.SieveAbstractPrefManager = SieveAbstractPrefManager; - else - exports.SieveAbstractPrefManager = SieveAbstractPrefManager; - -})(this); diff --git a/src/common/managesieve.ui/settings/logic/SieveAbstractPrefManager.mjs b/src/common/managesieve.ui/settings/logic/SieveAbstractPrefManager.mjs new file mode 100644 index 00000000..ddbfcc3d --- /dev/null +++ b/src/common/managesieve.ui/settings/logic/SieveAbstractPrefManager.mjs @@ -0,0 +1,234 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +/** + * Manages preferences. + */ +class SieveAbstractPrefManager { + + /** + * Initializes the preference manager. + * + * @param {string} [namespace] + * the optional preferences namespace. + */ + constructor(namespace) { + if ((typeof (namespace) === "undefined") || (namespace === null)) + namespace = ""; + + this.namespace = namespace; + } + + /** + * Returns the namespace which is added to all preferences + * + * @returns {string} + * the prefix as string. + */ + getNamespace() { + return this.namespace; + } + + /** + * Returns a specific value. + * @abstract + * + * @param {string} key + * the key which should be returned. + * @returns {object} + * the value or undefined in case it does not exist. + */ + async getValue(key) { + throw new Error(`Implement SieveAbstractPrefManager::getValue(${key})`); + } + + /** + * Sets and persists the given preference. + * @abstract + * + * @param {string} key + * the preference key which should be written. + * @param {object} value + * the key's value. + * @returns {SievePrefManager} + * a self reference. + */ + async setValue(key, value) { + throw new Error(`Implement SieveAbstractPrefManager::setValue(${key},${value})`); + } + + /** + * Returns the boolean value for the preference. + * + * @param {string} key + * the preference's key + * @param {boolean} [fallback] + * the fallback value in case the key does not exist. + * @returns {boolean} + * the key's value as boolean + */ + async getBoolean(key, fallback) { + const value = await this.getValue(key); + + if (typeof (value) === "undefined" || value === null) + return fallback; + + // Parse boolean + if ((value === true) || (value === "true")) + return true; + + if ((value === false) || (value === "false")) + return false; + + return fallback; + } + + /** + * Sets a boolean value for the given key. + * + * @param {string} key + * the preference's key + * @param {boolean} value + * the value which should be set + * @returns {SievePrefManager} + * a self reference + */ + async setBoolean(key, value) { + // ensure it is an boolean... + value = !!value; + + await this.setValue(key, value); + return this; + } + + /** + * Returns the string value for the preference. + * + * @param {string} key + * the preference key + * @param {string} [fallback] + * the fallback value in case the key does not exist. + * @returns {string} + * the key's value as string + */ + async getString(key, fallback) { + const value = await this.getValue(key); + + if (typeof (value) === "undefined" || value === null) + return fallback; + + return `${value}`; + } + + /** + * The string which should be set for th preference + * + * @param {string} key + * the preference key + * @param {string} value + * the key's value as string + * @returns {SievePrefManager} + * a self reference + */ + async setString(key, value) { + await this.setValue(key, `${value}`); + return this; + } + + /** + * Returns the integer value for the preference. + * + * @param {string} key + * the preference's key + * @param {int} [fallback] + * the fallback value in case the key does not exist or is not a number. + * @returns {string} + * the key's value as integer + */ + async getInteger(key, fallback) { + + let value = await this.getValue(key); + + if (typeof (value) === "undefined" || value === null || Number.isNaN(value)) + return fallback; + + if (Number.isInteger(value)) + return value; + + try { + value = Number.parseInt(value, 10); + } catch { + return fallback; + } + + if (Number.isNaN(value)) + return fallback; + + return value; + } + + /** + * Sets an integer value for the given key. + * + * @param {string} key + * the preference's key + * @param {int} value + * the integer value which should be set. + * @returns {SievePrefManager} + * a self reference. + */ + async setInteger(key, value) { + await this.setValue(key, Number.parseInt(value, 10)); + return this; + } + + /** + * Saves a complex value like an object for the given key. + * The object needs to be serializable to a json string. + * + * @param {string} key + * the preference's key + * @param {object} value + * the complex value which should be saved. + * @returns {SievePrefManager} + * a self reference. + */ + async setComplexValue(key, value) { + await this.setValue(key, JSON.stringify(value)); + return this; + } + + /** + * Returns the complex value for the given key. + * + * @param {string} key + * the preference's key. + * @param {object} fallback + * the fallback value in case the key does not exist. + * @returns {object} + * the key's complex value. + */ + async getComplexValue(key, fallback) { + const value = await this.getValue(key); + + if (typeof (value) === "undefined" || value === null) + return fallback; + + try { + return JSON.parse(value); + } catch { + return fallback; + } + } + +} + +export { SieveAbstractPrefManager }; diff --git a/src/common/managesieve.ui/settings/logic/SieveAbstractSecurity.mjs b/src/common/managesieve.ui/settings/logic/SieveAbstractSecurity.mjs new file mode 100644 index 00000000..4be291c7 --- /dev/null +++ b/src/common/managesieve.ui/settings/logic/SieveAbstractSecurity.mjs @@ -0,0 +1,52 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + + +/** + * Defines the security related settings for an account. + * It is a minimal, mozilla specific implementation. + */ +class SieveAbstractSecurity { + + /** + * Creates a new instance. + * + * @param {SieveAccount} account + * the account with is associated with this account. + */ + constructor(account) { + this.account = account; + } + + /** + * Gets the current security settings. In case it is set to true + * a secure connection shall be used. + * + * @returns {boolean} + * true in case a secure connection should be used. + **/ + async isSecure() { + return await true; + } + + /** + * Gets the currently configured sasl mechanism. + * + * @returns {string} + * the sasl mechanism + **/ + async getMechanism() { + return await "default"; + } + +} + +export { SieveAbstractSecurity }; diff --git a/src/common/managesieve.ui/settings/logic/SieveAccountSettings.js b/src/common/managesieve.ui/settings/logic/SieveAccountSettings.js deleted file mode 100644 index 7e8f3254..00000000 --- a/src/common/managesieve.ui/settings/logic/SieveAccountSettings.js +++ /dev/null @@ -1,67 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - const CONFIG_DEBUG_ACCOUNT = "debug"; - const DEFAULT_LOG_LEVEL = 0; - - /** - * Manages the accounts common settings. - */ - class SieveAccountSettings { - - /** - * Creates a new instance. - * - * @param {SieveAccount} account - * a reference to the parent sieve account. - */ - constructor(account) { - this.account = account; - } - - /** - * Gets the log levels for the given account. - * - * @returns {int} - * the current log level - */ - async getLogLevel() { - return await this.account.getConfig() - .getInteger(CONFIG_DEBUG_ACCOUNT, DEFAULT_LOG_LEVEL); - } - - /** - * Sets the log level for the given account. - * - * @param {int} level - * the new log level - * - * @returns {SieveAccountSettings} - * a self reference. - */ - async setLogLevel(level) { - await this.account.getConfig().setInteger(CONFIG_DEBUG_ACCOUNT, level); - return this; - } - - } - - if (typeof (module) !== "undefined" && module && module.exports) { - module.exports.SieveAccountSettings = SieveAccountSettings; - } else { - exports.SieveAccountSettings = SieveAccountSettings; - } - -})(this); diff --git a/src/common/managesieve.ui/settings/logic/SieveAccountSettings.mjs b/src/common/managesieve.ui/settings/logic/SieveAccountSettings.mjs new file mode 100644 index 00000000..9af1e2c1 --- /dev/null +++ b/src/common/managesieve.ui/settings/logic/SieveAccountSettings.mjs @@ -0,0 +1,57 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +const CONFIG_DEBUG_ACCOUNT = "debug"; +const DEFAULT_LOG_LEVEL = 0; + +/** + * Manages the accounts common settings. + */ +class SieveAccountSettings { + + /** + * Creates a new instance. + * + * @param {SieveAccount} account + * a reference to the parent sieve account. + */ + constructor(account) { + this.account = account; + } + + /** + * Gets the log levels for the given account. + * + * @returns {int} + * the current log level + */ + async getLogLevel() { + return await this.account.getConfig() + .getInteger(CONFIG_DEBUG_ACCOUNT, DEFAULT_LOG_LEVEL); + } + + /** + * Sets the log level for the given account. + * + * @param {int} level + * the new log level + * + * @returns {SieveAccountSettings} + * a self reference. + */ + async setLogLevel(level) { + await this.account.getConfig().setInteger(CONFIG_DEBUG_ACCOUNT, level); + return this; + } + +} + +export { SieveAccountSettings }; diff --git a/src/common/managesieve.ui/settings/logic/SieveEditorSettings.js b/src/common/managesieve.ui/settings/logic/SieveEditorSettings.js deleted file mode 100644 index dc81330a..00000000 --- a/src/common/managesieve.ui/settings/logic/SieveEditorSettings.js +++ /dev/null @@ -1,82 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - const DEFAULT_TAB_POLICY = true; - const DEFAULT_TAB_WIDTH = 2; - const DEFAULT_INDENTATION_POLICY = false; - const DEFAULT_INDENTATION_WIDTH = 2; - - /** - * Manages the sieve editor settings. - */ - class SieveEditorSettings { - - /** - * Create a new instance. - * - * @param {SievePrefManager} pref - * the pref manager to be used for this editor settings. - */ - constructor(pref) { - this.pref = pref; - } - - /** - * Sets an editor setting. - * - * @param {string} name - * the preference name - * @param {object} value - * the preference value - */ - async setValue(name, value) { - await this.pref.setValue(`editor.${name}`, value); - } - - /** - * Gets an editor settings. - * - * @param {string} name - * the preference name - * @returns {object} - * the editor settings value. - */ - async getValue(name) { - - if (name === "tabulator-policy") - return await this.pref.getBoolean("editor.tabulator-policy", DEFAULT_TAB_POLICY); - - if (name === "tabulator-width") - return await this.pref.getInteger("editor.tabulator-width", DEFAULT_TAB_WIDTH); - - if (name === "indentation-policy") - return await this.pref.getBoolean("editor.indentation-policy", DEFAULT_INDENTATION_POLICY); - - if (name === "indentation-width") - return await this.pref.getInteger("editor.indentation-width", DEFAULT_INDENTATION_WIDTH); - - if (name === "syntax-check") - return await this.pref.getBoolean("editor.syntax-check", true); - - throw new Error(`Unknown settings ${name}`); - } - } - - if (typeof (module) !== "undefined" && module && module.exports) - module.exports.SieveEditorSettings = SieveEditorSettings; - else - exports.SieveEditorSettings = SieveEditorSettings; - -})(this); diff --git a/src/common/managesieve.ui/settings/logic/SieveEditorSettings.mjs b/src/common/managesieve.ui/settings/logic/SieveEditorSettings.mjs new file mode 100644 index 00000000..4dee45ef --- /dev/null +++ b/src/common/managesieve.ui/settings/logic/SieveEditorSettings.mjs @@ -0,0 +1,73 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +const DEFAULT_TAB_POLICY = true; +const DEFAULT_TAB_WIDTH = 2; +const DEFAULT_INDENTATION_POLICY = false; +const DEFAULT_INDENTATION_WIDTH = 2; + +/** + * Manages the sieve editor settings. + */ +class SieveEditorSettings { + + /** + * Create a new instance. + * + * @param {SievePrefManager} pref + * the pref manager to be used for this editor settings. + */ + constructor(pref) { + this.pref = pref; + } + + /** + * Sets an editor setting. + * + * @param {string} name + * the preference name + * @param {object} value + * the preference value + */ + async setValue(name, value) { + await this.pref.setValue(`editor.${name}`, value); + } + + /** + * Gets an editor settings. + * + * @param {string} name + * the preference name + * @returns {object} + * the editor settings value. + */ + async getValue(name) { + + if (name === "tabulator-policy") + return await this.pref.getBoolean("editor.tabulator-policy", DEFAULT_TAB_POLICY); + + if (name === "tabulator-width") + return await this.pref.getInteger("editor.tabulator-width", DEFAULT_TAB_WIDTH); + + if (name === "indentation-policy") + return await this.pref.getBoolean("editor.indentation-policy", DEFAULT_INDENTATION_POLICY); + + if (name === "indentation-width") + return await this.pref.getInteger("editor.indentation-width", DEFAULT_INDENTATION_WIDTH); + + if (name === "syntax-check") + return await this.pref.getBoolean("editor.syntax-check", true); + + throw new Error(`Unknown settings ${name}`); + } +} + +export { SieveEditorSettings }; diff --git a/src/common/managesieve.ui/settings/logic/SieveSecuritySettings.js b/src/common/managesieve.ui/settings/logic/SieveSecuritySettings.js deleted file mode 100644 index 11a69de3..00000000 --- a/src/common/managesieve.ui/settings/logic/SieveSecuritySettings.js +++ /dev/null @@ -1,87 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - const PREF_MECHANISM = "security.mechanism"; - const PREF_TLS = "security.tls"; - - /** - * Manages the account's security related settings - */ - class SieveSecurity { - - /** - * Creates a new instance. - * - * @param {SieveAccount} account - * the account with is associated with this account. - */ - constructor(account) { - this.account = account; - } - - /** - * Gets the currently configured sasl mechanism. - * - * @returns {string} - * the sasl mechanism - **/ - async getMechanism() { - return await this.account.getConfig().getString(PREF_MECHANISM, "default"); - } - - /** - * Sets the sasl mechanism. - * - * @param {string} mechanism - * the sasl mechanism which should be used. - * - * @returns {SieveSecurity} - * a self reference - */ - async setMechanism(mechanism) { - await this.account.getConfig().setString(PREF_MECHANISM, mechanism); - return this; - } - - /** - * Gets the current security settings. In case it is set to true - * a secure connection shall be used. - * - * @returns {boolean} - * true in case a secure connection should be used. - **/ - async isSecure() { - return await this.account.getConfig().getBoolean(PREF_TLS, true); - } - - /** - * Defines if a secure connections shall be used. - * - * @param {boolean} value - * set to true for a secure connection. - * - * @returns {SieveSecurity} - * a self reference - */ - async setSecure(value) { - await this.account.getConfig().setBoolean(PREF_TLS, value); - return this; - } - - } - - exports.SieveSecurity = SieveSecurity; - -})(module.exports); diff --git a/src/common/managesieve.ui/settings/ui/SieveDebugSettingsUI.js b/src/common/managesieve.ui/settings/ui/SieveDebugSettingsUI.js deleted file mode 100644 index e8bd9708..00000000 --- a/src/common/managesieve.ui/settings/ui/SieveDebugSettingsUI.js +++ /dev/null @@ -1,237 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - /* global bootstrap */ - /* global SieveTemplate */ - - // eslint-disable-next-line no-magic-numbers - const LOG_ACCOUNT_REQUEST = (1 << 0); - // eslint-disable-next-line no-magic-numbers - const LOG_ACCOUNT_RESPONSE = (1 << 1); - // eslint-disable-next-line no-magic-numbers - const LOG_ACCOUNT_STATE = (1 << 2); - // eslint-disable-next-line no-magic-numbers - const LOG_ACCOUNT_STREAM = (1 << 3); - // eslint-disable-next-line no-magic-numbers - const LOG_ACCOUNT_SESSION_INFO = (1 << 4); - - // eslint-disable-next-line no-magic-numbers - const LOG_GLOBAL_IPC_MESSAGES = (1 << 0); - // eslint-disable-next-line no-magic-numbers - const LOG_GLOBAL_ACTION = (1 << 1); - // eslint-disable-next-line no-magic-numbers - const LOG_GLOBAL_WIDGET = (1 << 2); - // eslint-disable-next-line no-magic-numbers - const LOG_GLOBAL_I18N = (1 << 3); - - /** - * A UI renderer for the sieve debug settings dialog - */ - class SieveDebugSettingsUI { - - /** - * Initializes the settings - * @param {SieveAccount} account - * the account for which the settings edited. - */ - constructor(account) { - this.account = account; - } - - /** - * Renders the UI element into the dom. - */ - async render() { - const dialog = this.getDialog(); - - const levels = await this.account.send("account-settings-get-debug"); - - this.setAccountLogLevel(levels.account); - this.setGlobalLogLevel(levels.global); - - dialog.querySelector(".siv-settings-show-advanced") - .addEventListener("click", () => { this.showAdvanced(); }); - dialog.querySelector(".siv-settings-hide-advanced") - .addEventListener("click", () => { this.hideAdvanced(); }); - - this.hideAdvanced(); - - } - - /** - * Shows the settings dialog - */ - async show() { - - document.querySelector("#ctx").appendChild( - await (new SieveTemplate()).load("./settings/ui/settings.debug.tpl")); - - await this.render(); - - const dialog = this.getDialog(); - const modal = new bootstrap.Modal(dialog); - - modal.show(); - - dialog.querySelector(".sieve-settings-apply") - .addEventListener("click", () => { - this.save(); - modal.hide(); - }); - - await new Promise((resolve) => { - - dialog.addEventListener("hidden.bs.modal", () => { - modal.dispose(); - dialog.parentNode.removeChild(dialog); - - resolve(); - }); - }); - - } - - /** - * Reads the currently set account log level from the dialog. - * - * @returns {int} - * the account log level as integer. - */ - getAccountLogLevel() { - let level = 0x00; - - if (document.querySelector("#debugClientServer").checked === true) - level |= LOG_ACCOUNT_REQUEST; - - if (document.querySelector("#debugServerClient").checked === true) - level |= LOG_ACCOUNT_RESPONSE; - - if (document.querySelector("#debugSessionManagement").checked === true) - level |= LOG_ACCOUNT_SESSION_INFO; - - if (document.querySelector("#debugStateMachine").checked === true) - level |= LOG_ACCOUNT_STATE; - - if (document.querySelector("#debugRawDump").checked === true) - level |= LOG_ACCOUNT_STREAM; - - return level; - } - - /** - * Sets the account log level in the dialog. - * - * @param {int} level - * the account log level as integer - */ - setAccountLogLevel(level) { - - document.querySelector("#debugClientServer").checked = (level & LOG_ACCOUNT_REQUEST); - document.querySelector("#debugServerClient").checked = (level & LOG_ACCOUNT_RESPONSE); - document.querySelector("#debugSessionManagement").checked = (level & LOG_ACCOUNT_SESSION_INFO); - document.querySelector("#debugStateMachine").checked = (level & LOG_ACCOUNT_STATE); - document.querySelector("#debugRawDump").checked = (level & LOG_ACCOUNT_STREAM); - } - - /** - * Reads the currently set global log level from the dialog. - * - * @returns {int} - * the global log level as integer. - */ - getGlobalLogLevel() { - let level = 0x00; - - if (document.querySelector("#debugActions").checked) - level |= LOG_GLOBAL_ACTION; - - if (document.querySelector("#debugIpcMessages").checked) - level |= LOG_GLOBAL_IPC_MESSAGES; - - if (document.querySelector("#debugWidgets").checked) - level |= LOG_GLOBAL_WIDGET; - - if (document.querySelector("#debugI18n").checked) - level |= LOG_GLOBAL_I18N; - - return level; - } - - /** - * Sets the global log level in the dialog. - * - * @param {int} level - * the global log level as integer - */ - setGlobalLogLevel(level) { - document.querySelector("#debugActions").checked = (level & LOG_GLOBAL_ACTION); - document.querySelector("#debugIpcMessages").checked = (level & LOG_GLOBAL_IPC_MESSAGES); - document.querySelector("#debugWidgets").checked = (level & LOG_GLOBAL_WIDGET); - document.querySelector("#debugI18n").checked = (level & LOG_GLOBAL_I18N); - } - - /** - * Validates and saves the setting before closing the dialog. - * In case the settings are invalid an error message is displayed. - */ - async save() { - - const levels = { - account: this.getAccountLogLevel(), - global: this.getGlobalLogLevel() - }; - - await this.account.send("account-settings-set-debug", { "levels": levels }); - } - - /** - * Returns the currents dialogs UI Element. - * - * @returns {HTMLElement} - * the dialogs UI elements. - */ - getDialog() { - return document.querySelector("#dialog-settings-debug"); - } - - /** - * Shows the advanced setting - */ - showAdvanced() { - const parent = this.getDialog(); - - parent.querySelector(".siv-settings-advanced").style.display = ""; - parent.querySelector(".siv-settings-show-advanced").style.display = "none"; - parent.querySelector(".siv-settings-hide-advanced").style.display = ""; - } - - /** - * Hides the advanced settings - */ - hideAdvanced() { - const parent = this.getDialog(); - - parent.querySelector(".siv-settings-advanced").style.display = "none"; - parent.querySelector(".siv-settings-show-advanced").style.display = ""; - parent.querySelector(".siv-settings-hide-advanced").style.display = "none"; - } - - } - if (typeof (module) !== "undefined" && module !== null && module.exports) - module.exports = SieveDebugSettingsUI; - else - exports.SieveDebugSettingsUI = SieveDebugSettingsUI; - -})(this); diff --git a/src/common/managesieve.ui/settings/ui/SieveDebugSettingsUI.mjs b/src/common/managesieve.ui/settings/ui/SieveDebugSettingsUI.mjs new file mode 100644 index 00000000..835ab34d --- /dev/null +++ b/src/common/managesieve.ui/settings/ui/SieveDebugSettingsUI.mjs @@ -0,0 +1,262 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +/* global bootstrap */ + +import { SieveTemplate } from "./../../utils/SieveTemplate.mjs"; + +// eslint-disable-next-line no-magic-numbers +const LOG_ACCOUNT_REQUEST = (1 << 0); +// eslint-disable-next-line no-magic-numbers +const LOG_ACCOUNT_RESPONSE = (1 << 1); +// eslint-disable-next-line no-magic-numbers +const LOG_ACCOUNT_STATE = (1 << 2); +// eslint-disable-next-line no-magic-numbers +const LOG_ACCOUNT_STREAM = (1 << 3); +// eslint-disable-next-line no-magic-numbers +const LOG_ACCOUNT_SESSION_INFO = (1 << 4); +// eslint-disable-next-line no-magic-numbers +const LOG_ACCOUNT_TRACE = (1 << 5); + +// eslint-disable-next-line no-magic-numbers +const LOG_GLOBAL_IPC_MESSAGES = (1 << 0); +// eslint-disable-next-line no-magic-numbers +const LOG_GLOBAL_ACTION = (1 << 1); +// eslint-disable-next-line no-magic-numbers +const LOG_GLOBAL_WIDGET = (1 << 2); +// eslint-disable-next-line no-magic-numbers +const LOG_GLOBAL_I18N = (1 << 3); +// eslint-disable-next-line no-magic-numbers +const LOG_GLOBAL_TRACE = (1 << 5); + +/** + * A UI renderer for the sieve debug settings dialog + */ +class SieveDebugSettingsUI { + + /** + * Initializes the settings + * @param {SieveAccount} account + * the account for which the settings edited. + */ + constructor(account) { + this.account = account; + } + + /** + * Renders the UI element into the dom. + */ + async render() { + const dialog = this.getDialog(); + + const levels = await this.account.send("account-settings-get-debug"); + + this.setAccountLogLevel(levels.account); + this.setGlobalLogLevel(levels.global); + + dialog.querySelector(".siv-settings-show-advanced") + .addEventListener("click", () => { this.showAdvanced(); }); + dialog.querySelector(".siv-settings-hide-advanced") + .addEventListener("click", () => { this.hideAdvanced(); }); + + dialog.querySelector(".siv-settings-open-developer-tools") + .addEventListener("click", () => { this.openDeveloperTools(); }); + + dialog.querySelector(".siv-settings-reload-ui") + .addEventListener("click", () => { this.reloadApp(); }); + + + this.hideAdvanced(); + } + + /** + * Reloads the application. + */ + reloadApp() { + this.account.send("reload-ui"); + } + + /** + * Opens the developer tools. + */ + openDeveloperTools() { + this.account.send("open-developer-tools"); + } + + /** + * Shows the settings dialog + */ + async show() { + + document.querySelector("#ctx").append( + await (new SieveTemplate()).load("./settings/ui/settings.debug.html")); + + await this.render(); + + const dialog = this.getDialog(); + const modal = new bootstrap.Modal(dialog); + + modal.show(); + + dialog.querySelector(".sieve-settings-apply") + .addEventListener("click", () => { + this.save(); + modal.hide(); + }); + + await new Promise((resolve) => { + + dialog.addEventListener("hidden.bs.modal", () => { + modal.dispose(); + dialog.remove(); + + resolve(); + }); + }); + + } + + /** + * Reads the currently set account log level from the dialog. + * + * @returns {int} + * the account log level as integer. + */ + getAccountLogLevel() { + let level = 0x00; + + if (document.querySelector("#debugClientServer").checked === true) + level |= LOG_ACCOUNT_REQUEST; + + if (document.querySelector("#debugServerClient").checked === true) + level |= LOG_ACCOUNT_RESPONSE; + + if (document.querySelector("#debugSessionManagement").checked === true) + level |= LOG_ACCOUNT_SESSION_INFO; + + if (document.querySelector("#debugStateMachine").checked === true) + level |= LOG_ACCOUNT_STATE; + + if (document.querySelector("#debugRawDump").checked === true) + level |= LOG_ACCOUNT_STREAM; + + if (document.querySelector("#debugBackEndTrace").checked) + level |= LOG_ACCOUNT_TRACE; + + return level; + } + + /** + * Sets the account log level in the dialog. + * + * @param {int} level + * the account log level as integer + */ + setAccountLogLevel(level) { + + document.querySelector("#debugClientServer").checked = (level & LOG_ACCOUNT_REQUEST); + document.querySelector("#debugServerClient").checked = (level & LOG_ACCOUNT_RESPONSE); + document.querySelector("#debugSessionManagement").checked = (level & LOG_ACCOUNT_SESSION_INFO); + document.querySelector("#debugStateMachine").checked = (level & LOG_ACCOUNT_STATE); + document.querySelector("#debugRawDump").checked = (level & LOG_ACCOUNT_STREAM); + document.querySelector("#debugBackEndTrace").checked = (level & LOG_ACCOUNT_TRACE); + } + + /** + * Reads the currently set global log level from the dialog. + * + * @returns {int} + * the global log level as integer. + */ + getGlobalLogLevel() { + let level = 0x00; + + if (document.querySelector("#debugActions").checked) + level |= LOG_GLOBAL_ACTION; + + if (document.querySelector("#debugIpcMessages").checked) + level |= LOG_GLOBAL_IPC_MESSAGES; + + if (document.querySelector("#debugWidgets").checked) + level |= LOG_GLOBAL_WIDGET; + + if (document.querySelector("#debugI18n").checked) + level |= LOG_GLOBAL_I18N; + + if (document.querySelector("#debugFrontEndTrace").checked) + level |= LOG_GLOBAL_TRACE; + + return level; + } + + /** + * Sets the global log level in the dialog. + * + * @param {int} level + * the global log level as integer + */ + setGlobalLogLevel(level) { + document.querySelector("#debugActions").checked = (level & LOG_GLOBAL_ACTION); + document.querySelector("#debugIpcMessages").checked = (level & LOG_GLOBAL_IPC_MESSAGES); + document.querySelector("#debugWidgets").checked = (level & LOG_GLOBAL_WIDGET); + document.querySelector("#debugI18n").checked = (level & LOG_GLOBAL_I18N); + document.querySelector("#debugFrontEndTrace").checked = (level & LOG_GLOBAL_TRACE); + } + + /** + * Validates and saves the setting before closing the dialog. + * In case the settings are invalid an error message is displayed. + */ + async save() { + + const levels = { + account: this.getAccountLogLevel(), + global: this.getGlobalLogLevel() + }; + + await this.account.send("account-settings-set-debug", { "levels": levels }); + } + + /** + * Returns the currents dialogs UI Element. + * + * @returns {HTMLElement} + * the dialogs UI elements. + */ + getDialog() { + return document.querySelector("#dialog-settings-debug"); + } + + /** + * Shows the advanced setting + */ + showAdvanced() { + const parent = this.getDialog(); + + parent.querySelector(".siv-settings-advanced").style.display = ""; + parent.querySelector(".siv-settings-show-advanced").style.display = "none"; + parent.querySelector(".siv-settings-hide-advanced").style.display = ""; + } + + /** + * Hides the advanced settings + */ + hideAdvanced() { + const parent = this.getDialog(); + + parent.querySelector(".siv-settings-advanced").style.display = "none"; + parent.querySelector(".siv-settings-show-advanced").style.display = ""; + parent.querySelector(".siv-settings-hide-advanced").style.display = "none"; + } + +} + +export { SieveDebugSettingsUI }; diff --git a/src/common/managesieve.ui/settings/ui/settings.debug.tpl b/src/common/managesieve.ui/settings/ui/settings.debug.html index 7328c157..71dedf81 100644 --- a/src/common/managesieve.ui/settings/ui/settings.debug.tpl +++ b/src/common/managesieve.ui/settings/ui/settings.debug.html @@ -4,7 +4,7 @@ <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" data-i18n="settings.title"></h5> - <button type="button" class="btn-close" data-dismiss="modal" aria-label="Close"></button> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> @@ -36,6 +36,12 @@ <label class="form-check-label" for="debugSessionManagement" data-i18n="debug.transport.session"></label> </div> + <div class="custom-control custom-switch"> + <input type="checkbox" class="form-check-input" id="debugBackEndTrace" /> + <label class="form-check-label" for="debugBackEndTrace" + data-i18n="debug.transport.trace"></label> + </div> + <div class="siv-settings-advanced"> <hr /> <h5 data-i18n="debug.global.title"></h5> @@ -63,12 +69,21 @@ <input type="checkbox" class="form-check-input" id="debugI18n" /> <label data-i18n="debug.global.i18n" class="form-check-label" for="debugI18n"></label> </div> + + <div class="custom-control custom-switch"> + <input type="checkbox" class="form-check-input" id="debugFrontEndTrace" /> + <label class="form-check-label" for="debugFrontEndTrace" + data-i18n="debug.global.trace"></label> + </div> </div> </div> </div> <div class="modal-footer"> + <button data-i18n="debug.console.show" class="siv-settings-open-developer-tools btn btn-outline-secondary"></button> + <button data-i18n="debug.ui.reload" class="siv-settings-reload-ui btn btn-outline-secondary"></button> + <span class="flex-grow-1"></span> <button data-i18n="settings.more" class="siv-settings-show-advanced btn btn-outline-secondary"></button> <button data-i18n="settings.less" class="siv-settings-hide-advanced btn btn-outline-secondary"></button> diff --git a/src/common/managesieve.ui/utils/SieveAbstractIpcClient.js b/src/common/managesieve.ui/utils/SieveAbstractIpcClient.js deleted file mode 100644 index 2c517de0..00000000 --- a/src/common/managesieve.ui/utils/SieveAbstractIpcClient.js +++ /dev/null @@ -1,239 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - const { SieveUniqueId } = require("./SieveUniqueId.js"); - - const _requestHandlers = new Map(); - const _responseHandlers = new Map(); - - /** - * An abstract implementation for a inter process/frame communication. - */ - class SieveAbstractIpcClient { - - /** - * Generates a unique id - * @returns {string} - * a unique id - */ - static generateId() { - return (new SieveUniqueId()).generate(); - } - - /** - * Gets a logger instance. - * @abstract - * - * @returns {SieveLogger} - * a sieve logger instance - */ - static getLogger() { - throw new Error(`Implement getLogger`); - } - - /** - * Called every time a new ipc message was received. - * @param {Event} e - * the ipc message containing the data. - */ - static onMessage(e) { - const msg = this.parseMessageFromEvent(e); - - if (msg.isResponse === true) { - this.onResponse(msg); - return; - } - - if (msg.isRequest === true) { - this.onRequest(msg, e.source); - return; - } - - this.onError(e); - } - - /** - * Called upon an external request which requires a response. - * - * @param {object} request - * the response message containing the data. - * - * @param {object} source - * the object which emitted/created this message. - */ - static async onRequest(request, source) { - - this.getLogger().logIpcMessage(`OnRequest: ${JSON.stringify(request)}`); - - if (!_requestHandlers.has(request.subject)) { - this.getLogger().logIpcMessage(`Unknown subject ${request.subject} in ${window.location}`); - return; - } - - const handler = _requestHandlers.get(request.subject); - - const response = request; - response.isResponse = true; - - try { - if (!handler.has(request.action)) { - this.getLogger().logIpcMessage(`Unknown action ${request.action} in ${window.location}`); - throw new Error(`Unknown action ${request.action}`); - } - - response.payload = await (handler.get(request.action)(request)); - } catch (ex) { - response.error = ex.message; - this.getLogger().logIpcMessage(ex); - } - - this.dispatch(response, source); - } - - /** - * Called when a response to a request it received. - * - * @param {object} message - * the response message containing the data. - */ - static onResponse(message) { - - this.getLogger().logIpcMessage(`On Response: ${JSON.stringify(message)}`); - - const id = message.id; - - // Check if the id is known to us. - if (id === undefined || id === null) - return; - - if (!_responseHandlers.has(message.id)) - return; - - this.getLogger().logIpcMessage(`Callback for ${id}`); - - // Check the response handlers - const handler = _responseHandlers.get(id); - _responseHandlers.delete(id); - - handler(message); - } - - /** - * Sends a message via the ipc communication - * @abstract - * @param {string} message - * the message to be send. - * @param {Window} target - * the target which should receive the message - * @param {object} [origin] - * optional information about the origin. - */ - static dispatch(message, target, origin) { - throw new Error(`Implement me ${message} ${target} ${origin}`); - } - - /** - * Extracts the message from the message event. - * @abstract - * - * @param {Event} e - * the event which contains the message. - */ - static parseMessageFromEvent(e) { - throw new Error(`Implement me ${e} `); - } - - /** - * Extracts the message source from the event object. - * @abstract - * - * @param {Event} e - * the event which should be analyzed - * @returns {object} - * the message source - */ - static getSource(e) { - throw new Error(`Implement me ${e} `); - } - - /** - * Registers a request handler for the given action. - * The can be at most one handler per action. In case it already - * exists it will be replaced. - * - * @param {string} subject - * the subject name to listen to. All other subject will be ignored. - * @param {string} action - * the action's unique name. - * @param {Function} callback - * the callback which should be invoked upon a matching request. - */ - static setRequestHandler(subject, action, callback) { - if (!_requestHandlers.has(subject)) - _requestHandlers.set(subject, new Map()); - - _requestHandlers.get(subject).set(action, callback); - } - - /** - * Sends a message to the given target. - * - * @param {string} subject - * the messages subject name specifies who will receive the message. - * @param {string} action - * the action to be performed. - * @param {object} payload - * the payload to be send - * @param {Window} [target] - * the target which host the receiver. In case it is omitted "parent" is used. - * @returns {*} - * the messages response or an exception in case of an error. - */ - static async sendMessage(subject, action, payload, target) { - - const id = this.generateId(); - - const msg = JSON.stringify({ - id: id, - subject: subject, - action: action, - payload: payload, - isRequest: true - }); - - return await new Promise((resolve, reject) => { - - const onResponse = (message) => { - if (message.error) { - reject(message.error); - return; - } - - resolve(message.payload); - }; - - _responseHandlers.set(id, onResponse); - this.dispatch(msg, target); - }); - } - } - - // Require modules need to use export.module - if (typeof (module) !== "undefined" && module && module.exports) - module.exports.SieveAbstractIpcClient = SieveAbstractIpcClient; - else - exports.SieveAbstractIpcClient = SieveAbstractIpcClient; - -})(this); diff --git a/src/common/managesieve.ui/utils/SieveAbstractIpcClient.mjs b/src/common/managesieve.ui/utils/SieveAbstractIpcClient.mjs new file mode 100644 index 00000000..a0940f7c --- /dev/null +++ b/src/common/managesieve.ui/utils/SieveAbstractIpcClient.mjs @@ -0,0 +1,229 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +import { SieveUniqueId } from "./SieveUniqueId.mjs"; + +const _requestHandlers = new Map(); +const _responseHandlers = new Map(); + +/** + * An abstract implementation for a inter process/frame communication. + */ +class SieveAbstractIpcClient { + + /** + * Generates a unique id + * @returns {string} + * a unique id + */ + static generateId() { + return (new SieveUniqueId()).generate(); + } + + /** + * Gets a logger instance. + * @abstract + * + * @returns {SieveLogger} + * a sieve logger instance + */ + static getLogger() { + throw new Error(`Implement getLogger`); + } + + /** + * Called every time a new ipc message was received. + * @param {Event} e + * the ipc message containing the data. + */ + static onMessage(e) { + const msg = this.parseMessageFromEvent(e); + + if (msg.isResponse === true) { + this.onResponse(msg); + return; + } + + if (msg.isRequest === true) { + this.onRequest(msg, e.source); + return; + } + + this.onError(e); + } + + /** + * Called upon an external request which requires a response. + * + * @param {object} request + * the response message containing the data. + * + * @param {object} source + * the object which emitted/created this message. + */ + static async onRequest(request, source) { + + this.getLogger().logIpcMessage(`OnRequest: ${JSON.stringify(request)}`); + + if (!_requestHandlers.has(request.subject)) { + this.getLogger().logIpcMessage(`Unknown subject ${request.subject} in ${window.location}`); + return; + } + + const handler = _requestHandlers.get(request.subject); + + const response = request; + response.isResponse = true; + + try { + if (!handler.has(request.action)) { + this.getLogger().logIpcMessage(`Unknown action ${request.action} in ${window.location}`); + throw new Error(`Unknown action ${request.action}`); + } + + response.payload = await (handler.get(request.action)(request)); + } catch (ex) { + response.error = ex.message; + this.getLogger().logIpcMessage(ex); + } + + this.dispatch(response, source); + } + + /** + * Called when a response to a request it received. + * + * @param {object} message + * the response message containing the data. + */ + static onResponse(message) { + + this.getLogger().logIpcMessage(`On Response: ${JSON.stringify(message)}`); + + const id = message.id; + + // Check if the id is known to us. + if (id === undefined || id === null) + return; + + if (!_responseHandlers.has(message.id)) + return; + + this.getLogger().logIpcMessage(`Callback for ${id}`); + + // Check the response handlers + const handler = _responseHandlers.get(id); + _responseHandlers.delete(id); + + handler(message); + } + + /** + * Sends a message via the ipc communication + * @abstract + * @param {string} message + * the message to be send. + * @param {Window} target + * the target which should receive the message + * @param {object} [origin] + * optional information about the origin. + */ + static dispatch(message, target, origin) { + throw new Error(`Implement me ${message} ${target} ${origin}`); + } + + /** + * Extracts the message from the message event. + * @abstract + * + * @param {Event} e + * the event which contains the message. + */ + static parseMessageFromEvent(e) { + throw new Error(`Implement me ${e} `); + } + + /** + * Extracts the message source from the event object. + * @abstract + * + * @param {Event} e + * the event which should be analyzed + * @returns {object} + * the message source + */ + static getSource(e) { + throw new Error(`Implement me ${e} `); + } + + /** + * Registers a request handler for the given action. + * The can be at most one handler per action. In case it already + * exists it will be replaced. + * + * @param {string} subject + * the subject name to listen to. All other subject will be ignored. + * @param {string} action + * the action's unique name. + * @param {Function} callback + * the callback which should be invoked upon a matching request. + */ + static setRequestHandler(subject, action, callback) { + if (!_requestHandlers.has(subject)) + _requestHandlers.set(subject, new Map()); + + _requestHandlers.get(subject).set(action, callback); + } + + /** + * Sends a message to the given target. + * + * @param {string} subject + * the messages subject name specifies who will receive the message. + * @param {string} action + * the action to be performed. + * @param {object} payload + * the payload to be send + * @param {Window} [target] + * the target which host the receiver. In case it is omitted "parent" is used. + * @returns {*} + * the messages response or an exception in case of an error. + */ + static async sendMessage(subject, action, payload, target) { + + const id = this.generateId(); + + const msg = JSON.stringify({ + id: id, + subject: subject, + action: action, + payload: payload, + isRequest: true + }); + + return await new Promise((resolve, reject) => { + + const onResponse = (message) => { + if (message.error) { + reject(message.error); + return; + } + + resolve(message.payload); + }; + + _responseHandlers.set(id, onResponse); + this.dispatch(msg, target); + }); + } +} + +export { SieveAbstractIpcClient }; diff --git a/src/common/managesieve.ui/utils/SieveFakeRequire.js b/src/common/managesieve.ui/utils/SieveFakeRequire.js deleted file mode 100644 index cb364a66..00000000 --- a/src/common/managesieve.ui/utils/SieveFakeRequire.js +++ /dev/null @@ -1,126 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - - -(function (exports) { - - "use strict"; - - /** - * Gets the module for the given name. - * In case it is not loaded into the current scope an exception will be thrown. - * - * @param {string} name - * the unique module name. - * @returns {object} - * the module. - */ - function getModule(name) { - - if (exports.module && exports.module.exports && exports.module.exports[name]) - return exports.module.exports[name]; - - if (exports[name]) - return exports[name]; - - throw new Error(`Module ${name} not loaded into scope`); - } - - /** - * Gets a list of modules. - * It will throw an exception in case one or more of the module - * names could not be loaded from the current scope - * - * @param {...string} names - * a list of module names - * @returns {object} - * the modules. - */ - function getModules(...names) { - - const m = {}; - - for (const name of names) - m[name] = getModule(name); - - return m; - } - - const globals = new Map(); - globals.set("./SieveUniqueId.js", "SieveUniqueId"); - globals.set("./../../utils/SieveUniqueId.js", "SieveUniqueId"); - globals.set("./../utils/SieveUniqueId.js", "SieveUniqueId"); - - globals.set("./SieveAbstractIpcClient.js", "SieveAbstractIpcClient"); - - globals.set("./../utils/SieveTemplate.js", "SieveTemplate"); - globals.set("./SieveI18n.js", "SieveI18n"); - - globals.set("./SieveAbstractAccounts.js", "SieveAbstractAccounts"); - globals.set("./SieveAbstractAccount.js", "SieveAbstractAccount"); - globals.set("./SievePrefManager.js", "SievePrefManager"); - globals.set("./SieveHostSettings.js", "SieveHost"); - globals.set("./SieveAuthorizationSettings.js", "SieveAuthorization"); - globals.set("./SieveAuthenticationSettings.js", "SieveAuthentication"); - globals.set("./SieveSecuritySettings.js", "SieveSecurity"); - globals.set("./SieveAccountSettings.js", "SieveAccountSettings"); - globals.set("./SieveEditorSettings.js", "SieveEditorSettings"); - - globals.set("./../../utils/SieveLogger.js", "SieveLogger"); - globals.set("./SieveLogger.js", "SieveLogger"); - - globals.set("libs/managesieve.ui/settings/SieveAbstractMechanism.js", "SieveAbstractMechanism"); - globals.set( - "libs/managesieve.ui/settings/SieveAbstractAuthorization.js", - ["SieveAbstractAuthorization", "SieveDefaultAuthorization"]); - globals.set("libs/managesieve.ui/settings/SieveAbstractAuthentication.js", "SieveAbstractAuthentication"); - - - globals.set("libs/managesieve.ui/settings/SieveAbstractHost.js", "SieveAbstractHost"); - - globals.set("libs/managesieve.ui/settings/SievePrefManager.js", "SievePrefManager"); - globals.set("libs/managesieve.ui/settings/SieveAbstractPrefManager.js", "SieveAbstractPrefManager"); - - - - /** - * A fake CommonJs Module implementation. - * Temporarily needed as node does not yet support ES6 modules. - * And mozilla does not support CommonJS - * - * @param {string} module - * the module to be loaded - * - * @returns {object} - * the module or an exception. - */ - function fakeRequire(module) { - - if (!globals.has(module)) - throw new Error(`Module ${module} unknown to fake module loader`); - - let modules = globals.get(module); - - if (!Array.isArray(modules)) - modules = [modules]; - - return getModules(...modules); - } - - - // In case there is no require in our scope we add our fake. - if (typeof (exports.require) !== "undefined" && exports.require !== null) { - exports.require = fakeRequire(); - } - - - exports.require = fakeRequire; -})(this); diff --git a/src/common/managesieve.ui/utils/SieveI18n.js b/src/common/managesieve.ui/utils/SieveI18n.js deleted file mode 100644 index 0b4179ce..00000000 --- a/src/common/managesieve.ui/utils/SieveI18n.js +++ /dev/null @@ -1,205 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - const FIRST_ELEMENT = 0; - - const DEFAULT_LOCALE = "en-US"; - const DEFAULT_PATH = "./i18n/"; - - // A list with all supported languages. - const LANGUAGES = new Set(); - LANGUAGES.add("en-US"); - LANGUAGES.add("de-DE"); - - // Maps a language to a supported language. - const LANGUAGE_MAPPING = new Map(); - LANGUAGE_MAPPING.set("en", "en-US"); - LANGUAGE_MAPPING.set("de", "de-DE"); - - const { SieveLogger } = require("./SieveLogger.js"); - - let instance = null; - - /** - * A poor mans i18n helper class which provides help to translate strings. - */ - class SieveI18n { - - /** - * Initializes a new instance. - */ - constructor() { - this.entities = {}; - } - - /** - * Creates or returns an initialized i18n instance. - * It is guaranteed to be a singleton. - * - * @returns {SieveI18n} - * the logger instance. - */ - static getInstance() { - - if (instance === null) - instance = new SieveI18n(); - - return instance; - } - - /** - * Gets an instance of the default logger. - * - * @returns {SieveLogger} - * a reference to a logger instance. - */ - getLogger() { - return SieveLogger.getInstance(); - } - - - /** - * Tries to find a compatible and supported language. - * In case the language can not be mapped it will - * fallback to american english. - * - * @param {string} language - * a language string in BCP 47 format - * - * @returns {string} - * the best compatible locale. - */ - getLanguage(language) { - - // Check if it's a perfect match with a well known language region. - if (LANGUAGES.has(language)) - return language; - - // If not we split the language from the region... - language = language.split('-')[FIRST_ELEMENT].toLowerCase(); - - // ... and try to find the matching the language. - // in case it fails we fall back to the default. - if (!LANGUAGE_MAPPING.has(language)) - return DEFAULT_LOCALE; - - language = LANGUAGE_MAPPING.get(language); - - // Double check that our mapping really points to a supported language. - // if not we fall back to the default. - if (!LANGUAGES.has(language)) - return DEFAULT_LOCALE; - - return language; - } - - /** - * Loads translations for the given locale. - * - * @param {string} [locale] - * optional the locale to be loaded, if omitted or set to "default" - * the browser's default language (navigator.language) is used. - * @param {string} [path] - * the optional path to the directory where the locale files are stored. - * If omitted ./i18n is used - * @returns {SieveI18n} - * a self reference. - */ - async load(locale, path) { - - if (typeof (locale) === "undefined" || locale === null || locale === "default") - locale = navigator.language; - - if (typeof (path) === "undefined" || path === null) - path = DEFAULT_PATH; - - if (!path.endsWith("/")) - path = `${path}/`; - - this.getLogger().logI18n(`Language set to ${locale}`); - - locale = this.getLanguage(locale); - - this.getLogger().logI18n(`Language normalized to ${locale}`); - - try { - await this.loadDictionary(`${path}${locale}.json`); - } catch (ex) { - // In case loading the dictionary failed e.g. due to a parsing error - // we try falling back to our default one which is used during development. - await this.loadDictionary(`${path}${DEFAULT_LOCALE}.json`); - } - - return this; - } - - /** - * Loads a dictionary which is used to translate the strings. - * It will throw an exception in case the dictionary can not be loaded. - * - * @param {string} dictionary - * the path to the dictionary file. - * - * @returns {SieveI18n} - * a self reference - */ - async loadDictionary(dictionary) { - - let data = null; - - try { - data = await (await fetch(dictionary, { cache: "no-store" })).text(); - } catch (ex) { - this.getLogger().logI18n(`Failed to load dictionary ${dictionary}`); - throw new Error(`Failed to load dictionary ${dictionary}`); - } - - try { - this.entities = JSON.parse(data.replace(/^\s+\/\/.*$/gm, "")); - } catch (ex) { - this.getLogger().logI18n(`Parsing dictionary ${dictionary} failed with error ${ex}`); - } - - this.getLogger().logI18n(`Dictionary ${dictionary} loaded`); - - return this; - } - - /** - * Returns the translated string for the entity. - * In case no translation was found an exception is thrown. - * - * @param {string} entity - * the string which should be translated - * @returns {string} - * the translated string - */ - getString(entity) { - const value = this.entities[entity]; - - if (typeof (value) === "undefined" || value === null) - throw new Error(`No translation for ${entity}`); - - return value; - } - - } - - if (typeof (module) !== "undefined" && module && module.exports) - module.exports.SieveI18n = SieveI18n; - else - exports.SieveI18n = SieveI18n; - -})(this); diff --git a/src/common/managesieve.ui/utils/SieveI18n.mjs b/src/common/managesieve.ui/utils/SieveI18n.mjs new file mode 100644 index 00000000..fbe17c47 --- /dev/null +++ b/src/common/managesieve.ui/utils/SieveI18n.mjs @@ -0,0 +1,196 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +const FIRST_ELEMENT = 0; + +const DEFAULT_LOCALE = "en-US"; +const DEFAULT_PATH = "./i18n/"; + +// A list with all supported languages. +const LANGUAGES = new Set(); +LANGUAGES.add("en-US"); +LANGUAGES.add("de-DE"); + +// Maps a language to a supported language. +const LANGUAGE_MAPPING = new Map(); +LANGUAGE_MAPPING.set("en", "en-US"); +LANGUAGE_MAPPING.set("de", "de-DE"); + +import { SieveLogger } from "./SieveLogger.mjs"; + +let instance = null; + +/** + * A poor mans i18n helper class which provides help to translate strings. + */ +class SieveI18n { + + /** + * Initializes a new instance. + */ + constructor() { + this.entities = {}; + } + + /** + * Creates or returns an initialized i18n instance. + * It is guaranteed to be a singleton. + * + * @returns {SieveI18n} + * the logger instance. + */ + static getInstance() { + + if (instance === null) + instance = new SieveI18n(); + + return instance; + } + + /** + * Gets an instance of the default logger. + * + * @returns {SieveLogger} + * a reference to a logger instance. + */ + getLogger() { + return SieveLogger.getInstance(); + } + + + /** + * Tries to find a compatible and supported language. + * In case the language can not be mapped it will + * fallback to american english. + * + * @param {string} language + * a language string in BCP 47 format + * + * @returns {string} + * the best compatible locale. + */ + getLanguage(language) { + + // Check if it's a perfect match with a well known language region. + if (LANGUAGES.has(language)) + return language; + + // If not we split the language from the region... + language = language.split('-')[FIRST_ELEMENT].toLowerCase(); + + // ... and try to find the matching the language. + // in case it fails we fall back to the default. + if (!LANGUAGE_MAPPING.has(language)) + return DEFAULT_LOCALE; + + language = LANGUAGE_MAPPING.get(language); + + // Double check that our mapping really points to a supported language. + // if not we fall back to the default. + if (!LANGUAGES.has(language)) + return DEFAULT_LOCALE; + + return language; + } + + /** + * Loads translations for the given locale. + * + * @param {string} [locale] + * optional the locale to be loaded, if omitted or set to "default" + * the browser's default language (navigator.language) is used. + * @param {string} [path] + * the optional path to the directory where the locale files are stored. + * If omitted ./i18n is used + * @returns {SieveI18n} + * a self reference. + */ + async load(locale, path) { + + if (typeof (locale) === "undefined" || locale === null || locale === "default") + locale = navigator.language; + + if (typeof (path) === "undefined" || path === null) + path = DEFAULT_PATH; + + if (!path.endsWith("/")) + path = `${path}/`; + + this.getLogger().logI18n(`Language set to ${locale}`); + + locale = this.getLanguage(locale); + + this.getLogger().logI18n(`Language normalized to ${locale}`); + + try { + await this.loadDictionary(`${path}${locale}.json`); + } catch { + // In case loading the dictionary failed e.g. due to a parsing error + // we try falling back to our default one which is used during development. + await this.loadDictionary(`${path}${DEFAULT_LOCALE}.json`); + } + + return this; + } + + /** + * Loads a dictionary which is used to translate the strings. + * It will throw an exception in case the dictionary can not be loaded. + * + * @param {string} dictionary + * the path to the dictionary file. + * + * @returns {SieveI18n} + * a self reference + */ + async loadDictionary(dictionary) { + + let data = null; + + try { + data = await (await fetch(dictionary, { cache: "no-store" })).text(); + } catch (ex) { + this.getLogger().logI18n(`Loading dictionary ${dictionary} failed with error ${ex}`); + throw new Error(`Failed to load dictionary ${dictionary}`); + } + + try { + this.entities = JSON.parse(data.replace(/^\s+\/\/.*$/gm, "")); + } catch (ex) { + this.getLogger().logI18n(`Parsing dictionary ${dictionary} failed with error ${ex}`); + } + + this.getLogger().logI18n(`Dictionary ${dictionary} loaded`); + + return this; + } + + /** + * Returns the translated string for the entity. + * In case no translation was found an exception is thrown. + * + * @param {string} entity + * the string which should be translated + * @returns {string} + * the translated string + */ + getString(entity) { + const value = this.entities[entity]; + + if (typeof (value) === "undefined" || value === null) + throw new Error(`No translation for ${entity}`); + + return value; + } + +} + +export { SieveI18n }; diff --git a/src/common/managesieve.ui/utils/SieveLogger.js b/src/common/managesieve.ui/utils/SieveLogger.js deleted file mode 100644 index bee5916b..00000000 --- a/src/common/managesieve.ui/utils/SieveLogger.js +++ /dev/null @@ -1,227 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - const TWO_CHARS = 2; - const THREE_CHARS = 3; - const BASE_10 = 10; - - // eslint-disable-next-line no-magic-numbers - const LOG_IPC_MESSAGES = (1 << 0); - // eslint-disable-next-line no-magic-numbers - const LOG_ACTION = (1 << 1); - // eslint-disable-next-line no-magic-numbers - const LOG_UI = (1 << 2); - // eslint-disable-next-line no-magic-numbers - const LOG_I18N = (1 << 3); - - const DEFAULT_LEVEL = 0x00; - - let instance = null; - - /** - * Implements a common and platform independent logging interface. - * The log level is interpreted as a bit filed with turns logging - * for the specified scope on and of. - * - * The level is concerning scopes and does not differentiate between - * warning, error and info. - */ - class SieveLogger { - - /** - * Creates a new instance - * @param {int} [level] - * the logger level - * - */ - constructor(level) { - - if (typeof (level) === "undefined") - level = DEFAULT_LEVEL; - - this._level = level; - } - - /** - * Creates or returns a logger instance. - * It is guaranteed to be a singleton. - * - * @returns {SieveLogger} - * the logger instance. - */ - static getInstance() { - if (instance === null) - instance = new SieveLogger(); - - return instance; - } - - /** - * Logs an action related message which as triggered by the - * user on the backend - * - * @param {string} message - * the log message - * @returns {SieveLogger} - * a self reference - */ - logAction(message) { - return this.log(message, "Action", LOG_ACTION); - } - - /** - * Logs an ipc related log message. - * - * @param {string} message - * the log message - * @returns {SieveLogger} - * a self reference - */ - logIpcMessage(message) { - return this.log(message, "Ipc", LOG_IPC_MESSAGES); - } - - /** - * Logs an ui related message. - * - * @param {string} message - * the log message - * @returns {SieveLogger} - * a self reference - */ - logWidget(message) { - return this.log(message, "Ui", LOG_UI); - } - - /** - * Logs a i18n related message. - * - * @param {string} message - * the log message. - * @returns {SieveLogger} - * a self reference. - */ - logI18n(message) { - return this.log(message, "I18n", LOG_I18N); - } - - /** - * Logs the given message to the browser console. - * - * @param {string} message - * the message which should be logged - * @param {string} prefix - * the log messages prefix. - * @param {int} [level] - * the log level. If omitted the message will be always logged. - * @returns {SieveLogger} - * a self reference - */ - log(message, prefix, level) { - - if (!this.isLoggable(level)) - return this; - - if ((typeof (prefix) === "undefined") || prefix === null) - prefix = ""; - - // eslint-disable-next-line no-console - console.log(`[${this.getTimestamp()} ${prefix}] ${message}`); - return this; - } - - - /** - * Tests if the log level should log. - * - * @param {int} level - * the level which should be checked. - * @returns {boolean} - * true in case the log level is activated otherwise false - */ - isLoggable(level) { - if (typeof (level) === "undefined") - return true; - - return !!(this.level() & level); - } - - /** - * Gets and sets the log level to the given bit mask. - * Note that the log level is a bit mask, every bit in the - * bit mask corresponds to a special logger. - * - * In order to activate or deactivate a logger you need to - * get the level toggle the desired bits and set the new level. - * - * @param {int} [level] - * the desired log level as bit mask. - * @returns {int} - * the current log level - */ - level(level) { - if (typeof (level) !== "undefined") - this._level = level; - - return this._level; - } - - - /** - * Pads the given string with leading zeros - * @private - * - * @param {string} n - * the string which should be padded - * @param {int} m - * the maximum padding. - * - * @returns {string} - * the padded string - */ - _pad(n, m) { - - let str = n; - - for (let i = 0; i < m; i++) - if (n < Math.pow(BASE_10, i)) - str = '0' + str; - - return str; - } - - /** - * Gets the current time in iso format (hh:mm:ss.SSS) - * - * @returns {string} - * the current timestamp as string. - */ - getTimestamp() { - - const date = new Date(); - return this._pad(date.getHours(), TWO_CHARS) - + ":" + this._pad(date.getMinutes(), TWO_CHARS) - + ":" + this._pad(date.getSeconds(), TWO_CHARS) - + "." + this._pad(date.getMilliseconds(), THREE_CHARS); - } - - } - - if (typeof (module) !== "undefined" && module && module.exports) - module.exports.SieveLogger = SieveLogger; - else - exports.SieveLogger = SieveLogger; - -})(this); diff --git a/src/common/managesieve.ui/utils/SieveLogger.mjs b/src/common/managesieve.ui/utils/SieveLogger.mjs new file mode 100644 index 00000000..1839a068 --- /dev/null +++ b/src/common/managesieve.ui/utils/SieveLogger.mjs @@ -0,0 +1,226 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +const TWO_CHARS = 2; +const THREE_CHARS = 3; +const BASE_10 = 10; + +// eslint-disable-next-line no-magic-numbers +const LOG_IPC_MESSAGES = (1 << 0); +// eslint-disable-next-line no-magic-numbers +const LOG_ACTION = (1 << 1); +// eslint-disable-next-line no-magic-numbers +const LOG_UI = (1 << 2); +// eslint-disable-next-line no-magic-numbers +const LOG_I18N = (1 << 3); +// eslint-disable-next-line no-magic-numbers +const LOG_TRACE = (1 << 5); + +const DEFAULT_LEVEL = 0x00; + +let instance = null; + +/** + * Implements a common and platform independent logging interface. + * The log level is interpreted as a bit filed with turns logging + * for the specified scope on and of. + * + * The level is concerning scopes and does not differentiate between + * warning, error and info. + */ +class SieveLogger { + + /** + * Creates a new instance + * @param {int} [level] + * the logger level + * + */ + constructor(level) { + + if (typeof (level) === "undefined") + level = DEFAULT_LEVEL; + + this._level = level; + } + + /** + * Creates or returns a logger instance. + * It is guaranteed to be a singleton. + * + * @returns {SieveLogger} + * the logger instance. + */ + static getInstance() { + if (instance === null) + instance = new SieveLogger(); + + return instance; + } + + /** + * Logs an action related message which as triggered by the + * user on the backend + * + * @param {string} message + * the log message + * @returns {SieveLogger} + * a self reference + */ + logAction(message) { + return this.log(message, "Action", LOG_ACTION); + } + + /** + * Logs an ipc related log message. + * + * @param {string} message + * the log message + * @returns {SieveLogger} + * a self reference + */ + logIpcMessage(message) { + return this.log(message, "Ipc", LOG_IPC_MESSAGES); + } + + /** + * Logs an ui related message. + * + * @param {string} message + * the log message + * @returns {SieveLogger} + * a self reference + */ + logWidget(message) { + return this.log(message, "Ui", LOG_UI); + } + + /** + * Logs a i18n related message. + * + * @param {string} message + * the log message. + * @returns {SieveLogger} + * a self reference. + */ + logI18n(message) { + return this.log(message, "I18n", LOG_I18N); + } + + /** + * Logs the given message to the browser console. + * + * @param {string} message + * the message which should be logged + * @param {string} prefix + * the log messages prefix. + * @param {int} [level] + * the log level. If omitted the message will be always logged. + * @returns {SieveLogger} + * a self reference + */ + log(message, prefix, level) { + + if (!this.isLoggable(level)) + return this; + + if ((typeof (prefix) === "undefined") || prefix === null) + prefix = ""; + + if (this.isLoggable(LOG_TRACE)) { + // eslint-disable-next-line no-console + console.trace(`[${this.getTimestamp()} ${prefix}] ${message}`); + return this; + } + + // eslint-disable-next-line no-console + console.log(`[${this.getTimestamp()} ${prefix}] ${message}`); + return this; + } + + + /** + * Tests if the log level should log. + * + * @param {int} level + * the level which should be checked. + * @returns {boolean} + * true in case the log level is activated otherwise false + */ + isLoggable(level) { + if (typeof (level) === "undefined") + return true; + + return !!(this.level() & level); + } + + /** + * Gets and sets the log level to the given bit mask. + * Note that the log level is a bit mask, every bit in the + * bit mask corresponds to a special logger. + * + * In order to activate or deactivate a logger you need to + * get the level toggle the desired bits and set the new level. + * + * @param {int} [level] + * the desired log level as bit mask. + * @returns {int} + * the current log level + */ + level(level) { + if (typeof (level) !== "undefined") + this._level = level; + + return this._level; + } + + + /** + * Pads the given string with leading zeros + * @private + * + * @param {string} n + * the string which should be padded + * @param {int} m + * the maximum padding. + * + * @returns {string} + * the padded string + */ + _pad(n, m) { + + let str = n; + + for (let i = 0; i < m; i++) + if (n < Math.pow(BASE_10, i)) + str = '0' + str; + + return str; + } + + /** + * Gets the current time in iso format (hh:mm:ss.SSS) + * + * @returns {string} + * the current timestamp as string. + */ + getTimestamp() { + + const date = new Date(); + return this._pad(date.getHours(), TWO_CHARS) + + ":" + this._pad(date.getMinutes(), TWO_CHARS) + + ":" + this._pad(date.getSeconds(), TWO_CHARS) + + "." + this._pad(date.getMilliseconds(), THREE_CHARS); + } + +} + +export { SieveLogger }; diff --git a/src/common/managesieve.ui/utils/SieveTemplate.js b/src/common/managesieve.ui/utils/SieveTemplate.js deleted file mode 100644 index 40907519..00000000 --- a/src/common/managesieve.ui/utils/SieveTemplate.js +++ /dev/null @@ -1,118 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - const { SieveLogger } = require("./SieveLogger.js"); - const { SieveI18n } = require("./SieveI18n.js"); - - /** - * Loads an html fragment from a file or url. - */ - class SieveTemplate { - - /** - * Gets an instance of the default i18n - * - * @returns {SieveI18n} - * a reference to an i18n instance. - */ - getI18n() { - return SieveI18n.getInstance(); - } - - /** - * Gets an instance of the default logger. - * - * @returns {SieveLogger} - * a reference to a logger instance. - */ - getLogger() { - return SieveLogger.getInstance(); - } - - /** - * Translates a loaded template. - * It queries all data-i18n and translates all elements found. - * - * @param {DocumentFragment} fragment - * the template which should be translated. - * @returns {DocumentFragment} - * the translated template. - */ - translate(fragment) { - - // Check if a translator is attached to this loader... - // if ((typeof(this.i18n) === "undefined") || (this.i18n === null)) - // return fragment; - - // Get all elements with a data-i18n tag from the fragment. - for (const elm of fragment.querySelectorAll('[data-i18n]')) { - - const entity = elm.dataset.i18n; - - // We translate the placeholder on HTML Elements - if ((elm instanceof HTMLInputElement) && (elm.type === "text")) { - try { - elm.placeholder = this.getI18n().getString(entity); - } catch (ex) { - this.getLogger().logI18n(ex); - } - continue; - } - - // Warn if text content is not empty. - if (elm.textContent.trim() !== "") { - this.getLogger().logI18n(`Text node for ${entity} not empty, replacing existing text`); - } - - // Get the translation and update the text... - try { - elm.textContent = this.getI18n().getString(entity); - } catch (ex) { - this.getLogger().logI18n(ex); - elm.classList.add("alert-danger"); - elm.textContent = entity; - } - } - - return fragment; - } - - /** - * Loads an html fragment from file or url - * - * @param {string} tpl - * the path tho the template file - * @returns {Promise<HTMLElement>} - * the template which should be loaded. - */ - async load(tpl) { - - this.getLogger().logWidget(`Load template ${tpl}`); - - const html = await (await fetch(tpl, { cache: "no-store" })).text(); - - const doc = (new DOMParser()).parseFromString(html, "text/html"); - - return this.translate(doc.body.firstElementChild); - } - } - - - if (typeof (module) !== "undefined" && module && module.exports) - module.exports.SieveTemplate = SieveTemplate; - else - exports.SieveTemplate = SieveTemplate; - -})(this); diff --git a/src/common/managesieve.ui/utils/SieveTemplate.mjs b/src/common/managesieve.ui/utils/SieveTemplate.mjs new file mode 100644 index 00000000..514e5007 --- /dev/null +++ b/src/common/managesieve.ui/utils/SieveTemplate.mjs @@ -0,0 +1,108 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +import { SieveLogger } from "./SieveLogger.mjs"; +import { SieveI18n } from "./SieveI18n.mjs"; + +/** + * Loads an html fragment from a file or url. + */ +class SieveTemplate { + + /** + * Gets an instance of the default i18n + * + * @returns {SieveI18n} + * a reference to an i18n instance. + */ + getI18n() { + return SieveI18n.getInstance(); + } + + /** + * Gets an instance of the default logger. + * + * @returns {SieveLogger} + * a reference to a logger instance. + */ + getLogger() { + return SieveLogger.getInstance(); + } + + /** + * Translates a loaded template. + * It queries all data-i18n and translates all elements found. + * + * @param {DocumentFragment} fragment + * the template which should be translated. + * @returns {DocumentFragment} + * the translated template. + */ + translate(fragment) { + + // Check if a translator is attached to this loader... + // if ((typeof(this.i18n) === "undefined") || (this.i18n === null)) + // return fragment; + + // Get all elements with a data-i18n tag from the fragment. + for (const elm of fragment.querySelectorAll('[data-i18n]')) { + + const entity = elm.dataset.i18n; + + // We translate the placeholder on HTML Elements + if ((elm instanceof HTMLInputElement) && (elm.type === "text")) { + try { + elm.placeholder = this.getI18n().getString(entity); + } catch (ex) { + this.getLogger().logI18n(ex); + } + continue; + } + + // Warn if text content is not empty. + if (elm.textContent.trim() !== "") { + this.getLogger().logI18n(`Text node for ${entity} not empty, replacing existing text`); + } + + // Get the translation and update the text... + try { + elm.textContent = this.getI18n().getString(entity); + } catch (ex) { + this.getLogger().logI18n(ex); + elm.classList.add("alert-danger"); + elm.textContent = entity; + } + } + + return fragment; + } + + /** + * Loads an html fragment from file or url + * + * @param {string} tpl + * the path tho the template file + * @returns {Promise<HTMLElement>} + * the template which should be loaded. + */ + async load(tpl) { + + this.getLogger().logWidget(`Load template ${tpl}`); + + const html = await (await fetch(tpl, { cache: "no-store" })).text(); + + const doc = (new DOMParser()).parseFromString(html, "text/html"); + + return this.translate(doc.body.firstElementChild); + } +} + +export { SieveTemplate }; diff --git a/src/common/managesieve.ui/utils/SieveUniqueId.js b/src/common/managesieve.ui/utils/SieveUniqueId.js deleted file mode 100644 index 1786b8da..00000000 --- a/src/common/managesieve.ui/utils/SieveUniqueId.js +++ /dev/null @@ -1,43 +0,0 @@ -/* - * The contents of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via email - * from the author. Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - * - */ - -(function (exports) { - - // Enable Strict Mode - "use strict"; - - const ASCII = 36; - const SEED_OFFSET = 2; - const SEED_LENGTH = 16; - /** - * Generates a poor mans unique id. - * It simply combines the current time with a random number. - * Which should be more than sufficient for us. - */ - class SieveUniqueId { - - /** - * Creates a pseudo random alpha numerical id. - * @returns {string} - * the generated id. - */ - generate() { - // "" + Math.floor(Math.random() * 10000000).toString(16) + Date.now().toString(16) - return (new Date()).getTime().toString(ASCII) - + "-" + Math.random().toString(ASCII).substr(SEED_OFFSET, SEED_LENGTH); - } - } - - if (typeof (module) !== "undefined" && module && module.exports) - module.exports.SieveUniqueId = SieveUniqueId; - else - exports.SieveUniqueId = SieveUniqueId; - -})(this); diff --git a/src/common/managesieve.ui/utils/SieveUniqueId.mjs b/src/common/managesieve.ui/utils/SieveUniqueId.mjs new file mode 100644 index 00000000..3e1379b2 --- /dev/null +++ b/src/common/managesieve.ui/utils/SieveUniqueId.mjs @@ -0,0 +1,33 @@ +/* + * The contents of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via email + * from the author. Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + * + */ + +const ASCII = 36; +const SEED_OFFSET = 2; +const SEED_LENGTH = 16; +/** + * Generates a poor mans unique id. + * It simply combines the current time with a random number. + * Which should be more than sufficient for us. + */ +class SieveUniqueId { + + /** + * Creates a pseudo random alpha numerical id. + * @returns {string} + * the generated id. + */ + generate() { + // "" + Math.floor(Math.random() * 10000000).toString(16) + Date.now().toString(16) + return Date.now().toString(ASCII) + + "-" + Math.random().toString(ASCII).substr(SEED_OFFSET, SEED_LENGTH); + } +} + +export { SieveUniqueId }; diff --git a/src/wx/api/sieve/SieveAccountsApi.js b/src/wx/api/sieve/SieveAccountsApi.js index 201cfb23..be264dc2 100644 --- a/src/wx/api/sieve/SieveAccountsApi.js +++ b/src/wx/api/sieve/SieveAccountsApi.js @@ -11,8 +11,6 @@ (function (exports) { - "use strict"; - /* global ExtensionCommon */ /* global Components */ diff --git a/src/wx/api/sieve/SieveAccountsApi.json b/src/wx/api/sieve/SieveAccountsApi.json index 328b1a76..6c6967a3 100644 --- a/src/wx/api/sieve/SieveAccountsApi.json +++ b/src/wx/api/sieve/SieveAccountsApi.json @@ -8,22 +8,26 @@ "type": "function", "description": "Gets a password from the password store", "async": true, - "parameters": [{ + "parameters": [ + { "name": "id", "type": "string", "description": "The server's id" - }] + } + ] }, { "name": "getPrettyName", "type": "function", "description": "Gets a password from the password store", "async": true, - "parameters": [{ + "parameters": [ + { "name": "id", "type": "string", "description": "The account key" - }] + } + ] }, { "name": "getUsername", @@ -43,11 +47,13 @@ "type": "function", "description": "Gets a password from the password store", "async": true, - "parameters": [{ + "parameters": [ + { "name": "id", "type": "string", "description": "The account key" - }] + } + ] } ] } diff --git a/src/wx/api/sieve/SieveMenuApi.js b/src/wx/api/sieve/SieveMenuApi.js index f9dcab27..92fef2a2 100644 --- a/src/wx/api/sieve/SieveMenuApi.js +++ b/src/wx/api/sieve/SieveMenuApi.js @@ -11,8 +11,6 @@ (function (exports) { - "use strict"; - /* global ExtensionCommon */ /* global Components */ @@ -42,7 +40,7 @@ * true in case the element exists otherwise false. */ hasNode(id) { - const node = this.document.getElementById(id); + const node = this.document.querySelector(`#${id}`); if (!node) return false; @@ -60,7 +58,7 @@ * the dom element or throws an exception. */ getNode(id) { - const node = this.document.getElementById(id); + const node = this.document.querySelector(`#${id}`); if (!node) throw new Error(`Unknown element ${id}`); @@ -74,9 +72,9 @@ * the node to be removed. */ removeNode(id) { - const elm = this.document.getElementById(id); + const elm = this.document.querySelector(`#${id}`); if (elm) - elm.parentNode.removeChild(elm); + elm.remove(); } /** @@ -121,7 +119,7 @@ const ref = this.getNode(refId); this.removeNode(item.getId()); - ref.appendChild(item.createNode(this.document)); + ref.append(item.createNode(this.document)); } } @@ -384,7 +382,7 @@ onCommand: new ExtensionCommon.EventManager({ context, - name: "sieve.session.onCommand", + name: "sieve.menu.onCommand", register: (fire) => { const callback = async (windowsId, id) => { diff --git a/src/wx/api/sieve/SieveMenuApi.json b/src/wx/api/sieve/SieveMenuApi.json index 89814178..c3d7111b 100644 --- a/src/wx/api/sieve/SieveMenuApi.json +++ b/src/wx/api/sieve/SieveMenuApi.json @@ -8,31 +8,36 @@ "type": "function", "description": "Checks if the menu item exists.", "async": true, - "parameters": [{ - "name": "windowId", - "type": "string", - "description" : "The unique window id" - },{ - "name": "widgetId", - "type": "string", - "description" : "the widget unique id", - "additionalProperties" :true - }] + "parameters": [ + { + "name": "windowId", + "type": "string", + "description": "The unique window id" + }, + { + "name": "widgetId", + "type": "string", + "description": "the widget unique id", + "additionalProperties": true + } + ] }, { "name": "add", "type": "function", "description": "Adds the menu item, in case it exists it will be overwritten", "async": true, - "parameters": [{ + "parameters": [ + { "name": "windowId", "type": "string", - "description" : "The unique window id" - },{ + "description": "The unique window id" + }, + { "name": "widget", "type": "object", - "description" : "the widget description", - "additionalProperties" :true + "description": "the widget description", + "additionalProperties": true } ] }, @@ -41,16 +46,19 @@ "type": "function", "description": "Removes the menu item.", "async": true, - "parameters": [{ - "name": "windowId", - "type": "string", - "description" : "The unique window id" - },{ - "name": "widgetId", - "type": "string", - "description" : "the widget unique id", - "additionalProperties" :true - }] + "parameters": [ + { + "name": "windowId", + "type": "string", + "description": "The unique window id" + }, + { + "name": "widgetId", + "type": "string", + "description": "the widget unique id", + "additionalProperties": true + } + ] } ], "events": [ diff --git a/src/wx/api/sieve/SieveSessionApi.js b/src/wx/api/sieve/SieveSessionApi.js deleted file mode 100644 index e46dcadd..00000000 --- a/src/wx/api/sieve/SieveSessionApi.js +++ /dev/null @@ -1,310 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - /* global ExtensionCommon */ - /* global Components */ - /* global ChromeUtils */ - - const Cc = Components.classes; - const Ci = Components.interfaces; - - const logger = undefined; - // // eslint-disable-next-line no-console - // const logger = function(msg) { console.log(msg); }; - - /** - * Implements a webextension api for sieve session and connection management. - */ - class SieveSessionApi extends ExtensionCommon.ExtensionAPI { - /** - * @inheritdoc - */ - getAPI(context) { - - const rootUri = context.extension.rootURI; - const url = context.extension.getURL(); - - // The default case it loading from a jar file. - let requireUrl = rootUri.resolve("/modules/SieveRequire.jsm"); - - // But for debugging we are loading from a file url. Which is prohibited - // so we need to convert the file url into an extension url. - if (rootUri.schemeIs("file")) - requireUrl = `${url}/modules/SieveRequire.jsm`; - - const { Modules } = ChromeUtils.import(requireUrl); - const modules = new Modules( - `${url}/libs/libManageSieve/`, logger); - - const sessions = new Map(); - - context.callOnClose({ close: () => { - - // First ensure we exit all active sessions. - for (const item of sessions.values()) - item.disconnect(true); - sessions.clear(); - - // Then invalidate all the modules. - modules.invalidate(); - - // And finally unload the module loader. - Components.utils.unload(rootUri.resolve("/modules/SieveRequire.jsm")); - }}); - - const { SieveSession } = modules.require("./SieveSession.js"); - - return { - sieve: { - session: { - - onAuthenticate: new ExtensionCommon.EventManager({ - context, - name: "sieve.session.onAuthenticate", - register: (fire, id) => { - - const callback = async (hasPassword) => { - return await fire.async(hasPassword); - }; - - sessions.get(id).on("authenticate", callback); - - return () => { - if (sessions.has(id)) - sessions.get(id).on("authenticate"); - }; - } - }).api(), - - onAuthorize: new ExtensionCommon.EventManager({ - context, - name: "sieve.session.onAuthorize", - register: (fire, id) => { - - const callback = async () => { - return await fire.async(); - }; - - sessions.get(id).on("authorize", callback); - - return () => { - if (sessions.has(id)) - sessions.get(id).on("authorize"); - }; - } - }).api(), - - onCertError: new ExtensionCommon.EventManager({ - context, - name: "sieve.session.onCertError", - register: (fire, id) => { - - // It is just a notification so no need to wait. - const callback = (secInfo) => { - fire.async(secInfo); - }; - - sessions.get(id).on("certerror", callback); - - return () => { - if (sessions.has(id)) - sessions.get(id).on("certerror"); - }; - } - }).api(), - - onProxyLookup: new ExtensionCommon.EventManager({ - context, - name: "sieve.session.onProxyLookup", - register: (fire, id) => { - - const callback = async (host, port) => { - return await fire.async(host, port); - }; - - sessions.get(id).on("proxy", callback); - - return () => { - if (sessions.has(id)) - sessions.get(id).on("proxy"); - }; - } - }).api(), - - // eslint-disable-next-line require-await - async create(id, options) { - - if (sessions.has(id)) - throw new Error("Id already in use"); - - sessions.set(id, - new SieveSession(id, options)); - }, - - async destroy(id) { - if (!sessions.has(id)) - return; - - await this.disconnect(id, true); - sessions.delete(id); - }, - - async has(id) { - return await sessions.has(id); - }, - - // eslint-disable-next-line require-await - async addCertErrorOverride(hostname, port, rawDER, flags) { - - const overrideService = Cc["@mozilla.org/security/certoverride;1"] - .getService(Ci.nsICertOverrideService); - - const certdb = Cc["@mozilla.org/security/x509certdb;1"] - .getService(Ci.nsIX509CertDB); - - // The constructX509 has an incompatible change. - // While newer version consume an array, older version use strings. - // This magic is only needed for thunderbird 68 - let cert = null; - try { - // Try first with the new api.. - cert = certdb.constructX509(rawDER); - } catch (ex) { - - if (ex.name !== "NS_ERROR_FAILURE") - throw ex; - - // ... other wise the use the old api. - cert = ""; - for (const ch of rawDER) - cert += (String.fromCharCode(ch)); - - cert = certdb.constructX509(cert); - } - - overrideService.rememberValidityOverride( - hostname, port, - cert, - flags, - false); - }, - - async connect(id, host, port) { - await sessions.get(id).connect(host, port); - }, - - async disconnect(id, force) { - await sessions.get(id).disconnect(force); - }, - - async isConnected(id) { - if (!sessions.has(id)) - return false; - - return await sessions.get(id).isConnected(); - }, - - async capabilities(id) { - return await sessions.get(id).capabilities(); - }, - - async listScripts(id) { - return await sessions.get(id).listScripts(); - }, - - async putScript(id, name, body) { - return await sessions.get(id).putScript(name, body); - }, - - async getScript(id, name) { - return await sessions.get(id).getScript(name); - }, - - async renameScript(id, oldname, newname) { - return await sessions.get(id).renameScript(oldname, newname); - }, - - async deleteScript(id, name) { - return await sessions.get(id).deleteScript(name); - }, - - async activateScript(id, name) { - return await sessions.get(id).activateScript(name); - }, - - async checkScript(id, body) { - // TODO move the try catch into checkscript... - try { - await sessions.get(id).checkScript(body); - } catch (ex) { - // FIXME We need to rethrow incase checkscript returns a SieveServerException. - return ex.getResponse().getMessage(); - } - - return ""; - }, - - async probe(host, port) { - - const { Sieve } = modules.require("./SieveSession.js"); - const { SieveLogger } = modules.require("./SieveLogger.js"); - const { SieveInitRequest } = modules.require("./SieveRequest.js"); - - const sieve = new Sieve(new SieveLogger()); - - return await new Promise((resolve) => { - - const listener = { - - onInitResponse: function () { - resolve(true); - sieve.disconnect(); - }, - - onError: function () { - resolve(false); - sieve.disconnect(); - }, - - onTimeout: function () { - resolve(false); - sieve.disconnect(); - }, - - onDisconnect: function () { - // we are already disconnected.... - resolve(false); - } - }; - - const request = new SieveInitRequest(); - request.addErrorListener(listener.onError); - request.addResponseListener(listener.onInitResponse); - sieve.addRequest(request); - - sieve.addListener(listener); - - sieve.connect(host, port, false); - }); - } - } - } - }; - } - } - - exports.SieveSessionApi = SieveSessionApi; - -})(this); diff --git a/src/wx/api/sieve/SieveSessionApi.json b/src/wx/api/sieve/SieveSessionApi.json deleted file mode 100644 index 0e7accee..00000000 --- a/src/wx/api/sieve/SieveSessionApi.json +++ /dev/null @@ -1,390 +0,0 @@ -[ - { - "namespace": "sieve.session", - "description": "Sieve Session Management", - "functions": [ - { - "name": "create", - "type": "function", - "description": "Creates a new session", - "async":true, - "parameters":[{ - "name": "id", - "type": "string", - "description" : "The sessions unique id" - },{ - "name": "options", - "type": "object", - "description" : "Connection options as key value pairs", - "additionalProperties" :true - }] - }, - { - "name": "destroy", - "type": "function", - "description": "Destroys the given session", - "async":true, - "parameters":[{ - "name": "id", - "type": "string", - "description" : "The sessions unique id" - }] - }, - { - "name": "connect", - "type": "function", - "description": "Connects to a remote server.", - "async": true, - "parameters": [{ - "name": "id", - "type": "string", - "description" : "the server host name" - },{ - "name": "host", - "type": "string", - "description" : "the server host name" - },{ - "name": "port", - "type": "string", - "description" : "the server port" - }] - }, - { - "name": "has", - "type": "function", - "description": "Checks if the connection is known.", - "async": true, - "parameters": [{ - "name": "id", - "type": "string", - "description" : "the connection id" - }] - }, - { - "name": "disconnect", - "type": "function", - "description": "Disconnects from the remote server.", - "async": true, - "parameters": [{ - "name": "id", - "type": "string", - "description" : "the connection id" - },{ - "name": "force", - "type": "boolean", - "optional": true, - "description" : "If true the disconnect will be forced." - }] - }, - { - "name": "isConnected", - "type": "function", - "description": "Checks if the given id is connected", - "async": true, - "parameters": [{ - "name": "id", - "type": "string", - "description" : "the unique connection id" - }] - }, - - { - "name": "capabilities", - "type": "function", - "description": "Gets the server's capabilities", - "async": true, - "parameters": [{ - "name": "id", - "type": "string", - "description" : "the unique connection id" - }] - }, - { - "name": "listScripts", - "type": "function", - "description": "Lists the server's scripts", - "async": true, - "parameters": [{ - "name": "id", - "type": "string", - "description" : "the unique connection id" - }] - }, - { - "name": "putScript", - "type": "function", - "description": "Uploads a new or changed script", - "async": true, - "parameters": [{ - "name": "id", - "type": "string", - "description" : "the unique connection id" - },{ - "name": "name", - "type": "string", - "description" : "The script name" - }, - { - "name": "body", - "type": "string", - "description" : "The scripts body" - }] - }, - { - "name": "getScript", - "type": "function", - "description": "Gets a script from the server", - "async": true, - "parameters": [{ - "name": "id", - "type": "string", - "description" : "the unique connection id" - },{ - "name": "name", - "type": "string", - "description" : "The script name" - }] - }, - { - "name": "renameScript", - "type": "function", - "description": "Renames an existing script", - "async": true, - "parameters": [{ - "name": "id", - "type": "string", - "description" : "the unique connection id" - },{ - "name": "oldName", - "type": "string", - "description" : "The old script name" - },{ - "name": "newName", - "type": "string", - "description" : "The new script name" - }] - }, - { - "name": "deleteScript", - "type": "function", - "description": "Deletes an existing script", - "async": true, - "parameters": [{ - "name": "id", - "type": "string", - "description" : "the unique connection id" - },{ - "name": "name", - "type": "string", - "description" : "The script name" - }] - }, - { - "name": "activateScript", - "type": "function", - "description": "Activates or deactivates a script", - "async": true, - "parameters": [{ - "name": "id", - "type": "string", - "description" : "the unique connection id" - },{ - "name": "name", - "type": "string", - "optional": true, - "description" : "The active script" - }] - }, - { - "name": "checkScript", - "type": "function", - "description": "Check a script for syntax errors", - "async": true, - "parameters": [{ - "name": "id", - "type": "string", - "description" : "the unique connection id" - },{ - "name": "body", - "type": "string", - "description" : "The scripts body" - }] - }, - { - "name": "probe", - "type": "function", - "description": "Checks the connection to the server", - "async": true, - "parameters": [{ - "name": "host", - "type": "string", - "description" : "the server's hostname" - },{ - "name": "port", - "type": "string", - "description" : "the server's port" - }] - },{ - "name": "addCertErrorOverride", - "type": "function", - "description": "Adds a certificate error override for the given cert.", - "async": true, - "parameters": [ - { - "name" : "host", - "type" : "string", - "description" : "The hostname to override" - }, - { - "name" : "port", - "type" : "string", - "description" : "The port to override" - }, - { - "name": "rawDER", - "type": "array", - "description" : "The server's certificate", - "items": { - "type": "integer" - } - }, - { - "name": "flags", - "type": "integer", - "description": "The override flags." - } - ] - } - ], - "events": [ - { - "name": "onAuthenticate", - "type": "function", - "description" : "Called when authentication information is needed", - "parameters": [ - { - "name": "hasPassword", - "description": "True in case a password is needed otherwise false.", - "type" : "boolean" - } - ], - "returns": { - "type": "object", - "description" : "The authentication information.", - "properties": { - "username": { "type": "string" }, - "password": { "type": "string" } - } - }, - "extraParameters": [ - { - "name": "id", - "description": "The unique account id.", - "type": "string" - } - ] - }, - { - "name": "onAuthorize", - "type": "function", - "description" : "Called when authorization information is needed", - "parameters": [ - ], - "returns": { - "type" : "string", - "description" : "The authentication information." - }, - "extraParameters": [ - { - "name": "id", - "description": "The unique account id.", - "type": "string" - } - ] - }, - { - "name": "onCertError", - "type": "function", - "description" : "Called when a tls handshake failed", - "parameters": [ - { - "name": "securityInfo", - "type": "object", - "description": "The authentication information.", - "properties": { - "host": { - "type" : "string", - "description" : "The hostname for which the validation failed" - }, - "port": { - "type" : "string", - "description" : "The port for which te validation failed" - }, - "rawDER": { - "type": "array", - "description" : "The server's certificate", - "items": { - "type": "integer" - } - }, - "isDomainMismatch": { - "description" : "The domain does not match", - "type": "boolean" - }, - "isExtendedValidation": { - "description" : "The extended validation failed", - "type": "boolean" - }, - "isNotValidAtThisTime": { - "description" : "The certificate's validity expired", - "type": "boolean" - }, - "isUntrusted": { - "description" : "The certificate's is untrusted or self signed", - "type": "boolean" - } - } - } - ], - "returns": { - - }, - "extraParameters": [ - { - "name": "id", - "description": "The unique account id.", - "type": "string" - } - ] - }, - { - "name": "onProxyLookup", - "type": "function", - "description" : "Called when a proxy lookup is needed", - "parameters": [ - { - "name": "host", - "description": "the servers hostname", - "type" : "string" - }, - { - "name": "port", - "description": "the server port", - "type" : "string" - } - ], - "returns": { - "type" : "string", - "description" : "The proxy information." - }, - "extraParameters": [ - { - "name": "id", - "description": "The unique account id.", - "type": "string" - } - ] - } - ] - } -]
\ No newline at end of file diff --git a/src/wx/api/sieve/SieveSocketApi.js b/src/wx/api/sieve/SieveSocketApi.js new file mode 100644 index 00000000..89ad82a1 --- /dev/null +++ b/src/wx/api/sieve/SieveSocketApi.js @@ -0,0 +1,1030 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +(function (exports) { + + /* global ExtensionCommon */ + /* global Components */ + /* global ChromeUtils */ + /* global Services */ + + // Input & output stream constants. + const STREAM_BUFFERED = 0; + + const DEFAULT_SEGMENT_SIZE = 0; + const DEFAULT_SEGMENT_COUNT = 0; + + const TRANSPORT_SECURE = 1; + + const OLD_TRANSPORT_API = 4; + const REALLY_OLD_TRANSPORT_API = 5; + + const STRING_AS_HEX = 16; + + const Cc = Components.classes; + const Ci = Components.interfaces; + // const Cu = Components.utils; + const Cr = Components.results; + + + const HEX_PREFIX_LEN = 2; + const HEX_UINT16_LEN = 4; + const HEX_UINT32_LEN = 8; + const HEX_UINT48_LEN = 12; + + const STATE_CLOSED = 0; + const STATE_CONNECTING = 1; + const STATE_OPEN = 2; + const STATE_CLOSING = 3; + + const SOCKET_STATUS = { + // eslint-disable-next-line no-magic-numbers + 0x804B0003 : "resolving", + // eslint-disable-next-line no-magic-numbers + 0x804B000B : "resolved", + // eslint-disable-next-line no-magic-numbers + 0x804B0007 : "connecting", + // eslint-disable-next-line no-magic-numbers + 0x804B0004 : "connected", + // eslint-disable-next-line no-magic-numbers + 0x804B0005 : "sending", + // eslint-disable-next-line no-magic-numbers + 0x804B000A : "waiting", + // eslint-disable-next-line no-magic-numbers + 0x804B0006 : "receiving", + // eslint-disable-next-line no-magic-numbers + 0x804B000C : "tls handshake stated", + // eslint-disable-next-line no-magic-numbers + 0x804B000D : "tls handshake stopped" + }; + + const NS_BASE_STREAM_CLOSED = 0x80470002; + const NS_ERROR_FAILURE = 0x80004005; + const NS_FAILURE_FLAG = 0x80000000; + + // eslint-disable-next-line no-magic-numbers + const LOG_STATE = (1 << 2); + // eslint-disable-next-line no-magic-numbers + const LOG_TRACE = (1 << 5); + + /** + * A simple TCP socket implementation. + */ + class SieveSocket { + + /** + * Constructs a new Sieve Socket object. + * + * The constructor neither creates nor connects the socket. + * You need to do this by calling the corresponding functions. + * + * @param {string} host + * the host name to which to connect + * @param {int} port + * the host's port + * @param {int} level + * enable logging debug messages + */ + constructor(host, port, level) { + this.socket = null; + + this.host = host; + this.port = Number.parseInt(port, 10); + this.level = Number.parseInt(level, 10); + + this.outstream = null; + this.instream = null; + + this.binaryOutStream = null; + this.buffer = []; + + this.state = STATE_CLOSED; + + this.handler = {}; + + this.thread = Cc["@mozilla.org/thread-manager;1"] + .getService(Ci.nsIThreadManager) + .mainThread; + } + + /** + * Logs a message via the attached log handler. + * In case no log handler is attached the message will be silently + * discarded + * + * @param {string} message + * the message to be logger. + */ + log(message) { + + if (!(this.level & LOG_STATE)) + return; + + if (this.level & LOG_TRACE) { + // eslint-disable-next-line no-console + console.trace(`[${(new Date()).toISOString()}] [${this.port}] ${message}`); + return; + } + + // eslint-disable-next-line no-console + console.log(`[${(new Date()).toISOString()}] [${this.port}] ${message}`); + } + + + /** + * We need this wrapper for compatibility. Stating Thunderbird 69 + * Bug 1558726 (https://bugzilla.mozilla.org/show_bug.cgi?id=1558726) + * introduced a breaking change for this interface. + * + * Before the change the first parameter was an array and the second one + * the array length. After the change the length parameter was removed + * which causes all the other arguments to shift by one. + * + * @private + * + * @param {*} [proxyInfo] + * optional proxy information. + * + * @returns {nsISocketTransportService} + * the transport service or throws an exception in case creation failed. + */ + createTransport(proxyInfo) { + + const transportService = + Cc["@mozilla.org/network/socket-transport-service;1"] + .getService(Ci.nsISocketTransportService); + + // The createTransport API changed several times so we need some magic. + // + // We currently have to support three versions. Thunderbird before 69 take + // five arguments. Between 69 and 89 take four arguments and after 89 they + // are back to five. + + // We know if it is before 78 we need to test if it is the really old API + if (Services.vc.compare(Services.appinfo.platformVersion, "78.0") < 0) { + if (transportService.createTransport.length === REALLY_OLD_TRANSPORT_API) + return transportService.createTransport(["starttls"], TRANSPORT_SECURE, this.host, this.port, proxyInfo); + } + + // After 78 we know it is either the old or the new api. + if (transportService.createTransport.length === OLD_TRANSPORT_API) + return transportService.createTransport(["starttls"], this.host, this.port, proxyInfo); + + return transportService.createTransport(["starttls"], this.host, this.port, proxyInfo, null); + } + + /** + * Connects to the remote server via the given proxy. + * + * @param {*} [proxy] + * the optional proxy information. If omitted a proxy lookup is performed. + */ + connect(proxy) { + this.log(`[SieveSocketApi:connect()] Preparing connection to ${this.host}:${this.port}, ...`); + + // If we know the proxy setting, we can do a shortcut... + if (proxy) { + this.log("[SieveSocketApi:connect()] ... proxy info ready."); + this.onProxyAvailable(null, null, proxy[0], null); + return; + } + + this.log(`Lookup Proxy Configuration for x-sieve://${this.host}:${this.port} ...`); + + const ios = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService); + + // const uri = ios.newURI("x-sieve://" + this.host + ":" + this.port, null, null); + const uri = ios.newURI(`http://${this.host}:${this.port}`, null, null); + + const pps = Cc["@mozilla.org/network/protocol-proxy-service;1"] + .getService(Ci.nsIProtocolProxyService); + pps.asyncResolve(uri, 0, this); + + this.log("[SieveSocketApi:connect()] ... waiting for proxy info."); + } + + /** + * Checks if the socket is still alive. + * + * @returns {boolean} + * true in case the socket is alive otherwise false. + */ + isAlive() { + return this.socket.isAlive(); + } + + /** + * Starts upgrading to a secure connection. + * + * This calls is async an non blocking. In case the upgrade fails the + * connection will be automatically closed by thunderbird. Which will + * trigger onStopRequest to be called. The request object can then be used + * to analyze what caused the error. + */ + startTLS() { + + this.log("[SieveSocketApi:startTLS()] Requesting upgrade to secure..."); + + if (this.state !== STATE_OPEN) + throw new Error("Socket not in open state"); + + const control = this.socket.securityInfo + .QueryInterface(Ci.nsISSLSocketControl); + + if (!control) + throw new Error("Socket can not be upgraded."); + + control.StartTLS(); + + this.log("[SieveSocketApi:startTLS()] ... done"); + } + + /** + * Signals a change in the transport status. + * We use it only to detect the point when we are connected and + * ready to send and receive data. + * + * @param {nsITransport} transport + * the transport which triggered this status update + * @param {int} status + * the status as error code + * @param {int} progress + * the amount of data read or written depending on the status code. + * @param {int} progressMax + * the maximum amount of data which will be read or written. + */ + // eslint-disable-next-line no-unused-vars + onTransportStatus(transport, status, progress, progressMax) { + + this.log(`[SieveSocketApi:onTransportStatus()] Status change to ` + + `${SOCKET_STATUS[status]} (${status.toString(STRING_AS_HEX)}) ...` ); + + if (status !== Ci.nsISocketTransport.STATUS_CONNECTED_TO) { + this.log("[SieveSocketApi:onTransportStatus()] ... ignored" ); + return; + } + + this.state = STATE_OPEN; + this.registerAsyncWait(this.instream.QueryInterface(Ci.nsIAsyncInputStream)); + + this.log("[SieveSocketApi:onTransportStatus()] ... socket now in open state"); + } + + /** + * Wait async for the input stream to have new data. + * + * @param {nsIAsyncInputStream} stream + * the input stream for which should be waited. + */ + registerAsyncWait(stream) { + this.log(`[SieveSocketApi:registerAsyncWait()] Registering async callback ...` ); + + if (!stream) + throw new Error("Invalid input stream"); + + if (!stream.asyncWait) + throw new Error("Not an async stream"); + + const threads = Cc["@mozilla.org/thread-manager;1"] + .getService(Ci.nsIThreadManager); + + // this.log(threads.mainThread); + // this.log(threads.currentThread); + // this.log(threads.mainThreadEventTarget); + + stream + .asyncWait(this, 0, 0, threads.mainThread); + + this.log(`[SieveSocketApi:registerAsyncWait()] ... done` ); + } + + + /** + * Analyzes if the exception is an error and if so it calls the error + * handler if available. + * + * @param {Error} ex + * the error to be analyzed + */ + onInputStreamError(ex) { + + this.log(`[SieveSocketApi:onInputStreamError()] Parsing error information...`); + let status = NS_ERROR_FAILURE; + + if (ex.result) + status = ex.result; + + // In case we are closing the socket we expect a stream closed error... + if (((this.state === STATE_CLOSING) || (this.state === STATE_CLOSED)) && (status === NS_BASE_STREAM_CLOSED)) { + this.log(`[SieveSocketApi:onInputStreamError()] ... skipping, closing stream on a closing socket`); + return; + } + + // In case is it is no error code we can skip. + if (!(status & NS_FAILURE_FLAG)) { + this.log(`[SieveSocketApi:onInputStreamError()] ... skipping, ` + + `${status.toString(STRING_AS_HEX)} is not an error code`); + return; + } + + const error = this.getError(status); + + if (this.handler && this.handler.onError) { + this.log(`[SieveSocketApi:onInputStreamError()] ... calling error handler ...`); + (async () => { this.handler.onError(error); })(); + } + + this.log(`[SieveSocketApi:onInputStreamError()] ... done`); + return; + } + + /** + * Calls by the async wait method whenever new data is available + * or the stream was closed. + * + * @param {nsIAsyncInputStream} stream + * the stream with the input data. + */ + onInputStreamReady(stream) { + + this.log(`[SieveSocketApi:onInputStreamReady()] Reading data...`); + + try { + // Check if we have data. + // This will throw in case the connection was refused or closed. + const count = stream.available(); + + // Ok we got data, so let's read it. + const data = this.binaryInputStream.readByteArray(count); + + this.log(`[SieveSocketApi:onInputStreamReady()] Reading ${count} bytes...`); + + // And then make sure we get notified as soon as new data arrives. + this.registerAsyncWait(stream.QueryInterface(Ci.nsIAsyncInputStream)); + + // Now it is time to process the data. + if ((data.length) && (this.handler && this.handler.onData)) + (async() => { this.handler.onData(data); })(); + + } catch (ex) { + + this.log(`[SieveSocketApi:onInputStreamReady()] ... failed with an error...`); + + // We ignore any errors after being closed + if (this.state === STATE_CLOSED) { + this.log(`[SieveSocketApi:onInputStreamReady()] ... ignoring error socket is closed`); + return; + } + + this.log(`[SieveSocketApi:onInputStreamReady()] ... processing error...`); + this.onInputStreamError(ex); + + this.log(`[SieveSocketApi:onInputStreamReady()] ... disconnecting socket...`); + this.disconnect(); + this.state = STATE_CLOSED; + + if (this.handler && this.handler.onError) { + this.log(`[SieveSocketApi:onInputStreamReady()] ... calling close handler...`); + (async () => { this.handler.onClose(); })(); + } + } + + this.log(`[SieveSocketApi:onInputStreamReady()] ... done`); + } + + /** + * Sends the given data via the socket to the remote sever. + * + * @param {byte[]} bytes + * the data to be send as byte array. + */ + send(bytes) { + this.log(`[SieveSocketApi:send()] Sending ${bytes.length} bytes...`); + + if (this.state !== STATE_OPEN) { + this.log(`[SieveSocketApi:send()] ... error socket is not open`); + throw new Error("Socket not in open state"); + } + + this.buffer.push(bytes); + + if (this.buffer.length) { + this.registerAsyncWait( + this.outstream.QueryInterface(Ci.nsIAsyncOutputStream)); + } + + // this.binaryOutStream.writeByteArray(bytes, bytes.length); + + this.log(`[SieveSocketApi:send()] ... done`); + } + + /** + * Called by the async wait method whenever a write was requested and + * successfully scheduled. + * + * @param {nsIAsyncOutputStream} stream + * the output which cause this callback. + */ + // eslint-disable-next-line no-unused-vars + onOutputStreamReady(stream) { + this.log(`[SieveSocketApi:onOutputStreamReady()] Writing data...`); + + if (!this.buffer || !this.buffer.length) { + this.log(`[SieveSocketApi:onOutputStreamReady()] ... buffer is empty.`); + return; + } + + while (this.buffer.length) { + const bytes = this.buffer.shift(); + + this.log(`[SieveSocketApi:onOutputStreamReady()] ... sending from ${bytes.length} buffer...`); + + this.binaryOutStream.writeByteArray(bytes, bytes.length); + this.binaryOutStream.flush(); + } + + this.log(`[SieveSocketApi:onOutputStreamReady()] ... done`); + + return; + } + + /** + * Disconnects from the remote socket. + * In case the socket is already disconnected it will fail silently. + * So calling disconnect multiple times is perfectly fine. + * + * After disconnecting the onClose handler will be fired. + */ + disconnect() { + + this.log("[SieveSocketApi:Disconnect()] Disconnecting socket..." ); + + if (this.state === STATE_CLOSED || this.state === STATE_CLOSING) { + this.log("[SieveSocketApi:Disconnect()] ... already done" ); + return; + } + + this.state = STATE_CLOSING; + + if (this.outstream) { + this.outstream.close(); + this.outstream = null; + } + + if (this.instream) { + this.instream.close(); + this.instream = null; + } + + this.socket.close(0); + this.socket = 0; + + this.log("[SieveSocketApi:Disconnect()] ... done" ); + } + + /** + * Attaches or detaches an event listener to this socket + * + * Existing event listeners with the same name will be replace. + * In case the callback is omitted the existing event listener will be removed. + * + * Currently the following listeners are supported: + * + * data - called upon data received. + * error - called in case the connection was terminated because of an error. + * Will be directly followed by a close message. + * close - called whenever the connection was closed. + * log - called whenever something should forwarded to the logger. + * + * Unknown names will fail with an exception. + * + * @param {string} name + * the event listener's name + * @param {Function} [callback] + * the optional callback to be invoked upon the event. It will replace + * the previous event listener with the same name. + */ + on(name, callback) { + if (name === "data") { + this.handler.onData = callback; + return; + } + + if (name === "error") { + this.handler.onError = callback; + return; + } + + if (name === "close") { + this.handler.onClose = callback; + return; + } + + throw new Error(`Invalid event handler ${name}`); + } + + + /** + * Checks if the error code is from the certificate error class. + * @param {int} code + * the code to be checked. + * + * @returns {boolean} + * true in case it is a certificate error otherwise false. + */ + isCertError(code) { + try { + const nssErrorsService = Cc["@mozilla.org/nss_errors_service;1"] + .getService(Ci.nsINSSErrorsService); + + const errorClass = nssErrorsService.getErrorClass(code); + if (errorClass === Ci.nsINSSErrorsService.ERROR_CLASS_BAD_CERT) + return true; + + } catch { + this.log(`Failed to extract error class`); + } + + return false; + } + + /** + * Tries to get the reason why the certificate handshake failed. + * + * @returns {Exception} + * an exception which describes the certificate error or null + */ + getCertError() { + this.log(`[SieveSocketApi:getCertError()] Converting cert error...`); + + if (!this.socket || !this.socket.securityInfo) { + this.log(`[SieveSocketApi:getCertError()] ... not a secure socket`); + return null; + } + + const secInfo = this.socket.securityInfo.QueryInterface(Ci.nsITransportSecurityInfo); + + const rv = { + "type" : "CertValidationError", + "host": this.host, + "port": this.port, + + "rawDER": secInfo.serverCert.getRawDER({}), + "fingerprint": secInfo.serverCert.sha256Fingerprint, + + "message": secInfo.errorCodeString, + "isDomainMismatch": secInfo.isDomainMismatch, + "isExtendedValidation": secInfo.isExtendedValidation, + "isNotValidAtThisTime": secInfo.isNotValidAtThisTime, + "isUntrusted": secInfo.isUntrusted + }; + + this.log(`[SieveSocketApi:getCertError()] ... done`); + return rv; + } + + /** + * Converts the status/error code into an socket error object. + * + * It load the official description if available. + * + * @param {int} status + * the error code to be analyzed + * + * @returns {object} + * a serializable message containing the error details. + */ + getSocketError(status) { + + this.log(`[SieveSocketApi:getSocketError()] Converting socket error ${status.toString(STRING_AS_HEX)}...`); + + const error = { + "type": "SocketError", + "status": status, + "message" : `Socket failed with error ${status.toString(STRING_AS_HEX)}` + }; + + try { + const nssErrorsService = Cc["@mozilla.org/nss_errors_service;1"] + .getService(Ci.nsINSSErrorsService); + + error.message = nssErrorsService.getErrorMessage(status); + this.log(`[SieveSocketApi:getSocketError()] ... ${error.message} ...`); + } catch { + // do nothing here we fallback to our generic default. + } + + this.log(`[SieveSocketApi:getSocketError()] ... done`); + return error; + } + + /** + * Converts the status code into an error object + * which can be transferred via ipc. + * + * @param {int} status + * the status code which should be analyzed. + * + * @returns {object} + * the error object. + */ + getError(status) { + + if (this.isCertError(status)) + return this.getCertError(); + + return this.getSocketError(status); + } + + /** + * An async callback for the proxy lookup. + * + * @private + * @param {nsIRequest} request + * the request for which the proxy information as requested. + * @param {nsIChannel} channel + * the channel for which the proxy information was requested. + * @param {nsIProxyInfo} aProxyInfo + * the proxy information from the lookup, if null a direct + * connection will be used. + * @param {int} status + * the failure code in case the proxy could not be resolved. + **/ + // eslint-disable-next-line no-unused-vars + onProxyAvailable(request, channel, aProxyInfo, status) { + + if (aProxyInfo) + this.log(`Using Proxy: [${aProxyInfo.type}] ${aProxyInfo.host}:${aProxyInfo.port}`); + else + this.log("Using Proxy: Direct"); + + this.socket = this.createTransport(aProxyInfo); + + this.state = STATE_CONNECTING; + + this.socket.setEventSink(this, this.thread); + + this.instream = this.socket.openInputStream( + STREAM_BUFFERED, DEFAULT_SEGMENT_SIZE, DEFAULT_SEGMENT_COUNT); + this.outstream = this.socket.openOutputStream( + STREAM_BUFFERED, DEFAULT_SEGMENT_SIZE, DEFAULT_SEGMENT_COUNT); + + // this.registerAsyncWait(this.instream.QueryInterface(Ci.nsIAsyncInputStream)); + + this.binaryInputStream = Cc["@mozilla.org/binaryinputstream;1"] + .createInstance(Ci.nsIBinaryInputStream); + this.binaryInputStream.setInputStream(this.instream); + + + this.binaryOutStream = + Cc["@mozilla.org/binaryoutputstream;1"] + .createInstance(Ci.nsIBinaryOutputStream); + + this.binaryOutStream.setOutputStream(this.outstream); + } + + + /** + * Implements the Gecko Component Manager's interfaces. + * So that the JavaScript code can be passed as interface + * to C++. + * + * In case the interface is not supported an NS_ERROR_NO_INTERFACE + * will be thrown. + * + * @param {nsIIDRef} uuid + * the interface's unique id to check. + * + * @returns {SieveMozClient} + * a self reference. + */ + QueryInterface(uuid) { + if (uuid.equals(Ci.nsISupports)) + return this; + // onProxyAvailable... + if (uuid.equals(Ci.nsIProtocolProxyCallback)) + return this; + // onInputStreamReady + if (uuid.equals(Ci.nsIInputStreamCallback)) + return this; + // onTransportStatus + if (uuid.equals(Ci.nsITransportEventSink)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + } + + } + + + // We use this to track all open sockets. + // So that we can do a clean shutdown. + const sockets = new Map(); + + /** + * Checks if the given socket exists. + * + * @param {string} socket + * the sockets unique id. + * @returns {boolean} + * true in case the socket exists otherwise false. + */ + function hasSocket(socket) { + return sockets.has(socket); + } + + /** + * Gets the socket for the given id. + * + * @param {string} socket + * the sockets unique id. + * @returns {SieveSocket} + * a reference to the socket object or an exception. + */ + function getSocket(socket) { + if (!sockets.has(socket)) + throw new Error(`Unknown socket id ${socket}`); + + return sockets.get(socket); + } + + + /** + * Implements a webextension api for sieve session and connection management. + */ + class SieveSocketApi extends ExtensionCommon.ExtensionAPI { + + /** + * @inheritdoc + */ + onStartup() { + // we're not actually interested in startup, we need the event only + // to ensure this experiment gets loaded. + } + + /** + * @inheritdoc + */ + onShutdown(isAppShutdown) { + if (isAppShutdown) { + return; + } + + // Clear caches that could prevent upgrades from working properly + const { Services } = ChromeUtils.import( + "resource://gre/modules/Services.jsm"); + Services.obs.notifyObservers(null, "startupcache-invalidate", null); + } + + /** + * @inheritdoc + */ + getAPI(context) { + + context.callOnClose({ + close: () => { + for (const key of sockets.keys()) { + sockets[key].destroy(); + sockets.delete(key); + } + } + }); + + return { + sieve: { + socket: { + + /** + * Creates a socket which can be connected to the remote server. + * + * @param {string} host + * the remote server's hostname. + * @param {string} port + * the remote server's port. + * @param {int} level + * true in case logging should be enabled. + * + * @returns {string} + * return the unique id. + */ + async create(host, port, level) { + + const socket = new SieveSocket(host, port, level); + + const id = Math.random().toString(STRING_AS_HEX).substr(HEX_PREFIX_LEN, HEX_UINT32_LEN) + + "-" + Math.random().toString(STRING_AS_HEX).substr(HEX_PREFIX_LEN, HEX_UINT16_LEN) + + "-" + Math.random().toString(STRING_AS_HEX).substr(HEX_PREFIX_LEN, HEX_UINT16_LEN) + + "-" + Math.random().toString(STRING_AS_HEX).substr(HEX_PREFIX_LEN, HEX_UINT16_LEN) + + "-" + Math.random().toString(STRING_AS_HEX).substr(HEX_PREFIX_LEN, HEX_UINT48_LEN); + + sockets.set(id, socket); + + return id; + }, + + /** + * Connects the socket to the remote server. + * + * @param {string} socket + * the socket's unique id. + */ + async connect(socket) { + await (getSocket(socket).connect()); + }, + + /** + * Upgrades the socket to be secure. + * + * @param {string} socket + * the socket's unique id. + */ + async startTLS(socket) { + await (getSocket(socket).startTLS()); + }, + + /** + * Sends data to the remote socket. + * + * @param {string} socket + * the socket's unique id. + * @param {int[]} bytes + * the data to send as byte array. + */ + async send(socket, bytes) { + await (getSocket(socket).send(bytes)); + }, + + /** + * Checks if the socket isAlive. + * + * @param {string} socket + * the socket's unique id. + * @returns {boolean} + * true in case the socket is active otherwise false. + */ + async isAlive(socket) { + if (!hasSocket(socket)) + return false; + + return await (getSocket(socket).isAlive()); + }, + + /** + * Disconnects from the remote server. + * + * @param {string} socket + * the socket's unique id. + */ + async disconnect(socket) { + + // We can skip in case the socket is already cleaned up. + if (!hasSocket(socket)) + return; + + await (getSocket(socket).disconnect()); + }, + + /** + * Destroys and disconnects the remote server connection. + * + * @param {string} socket + * the socket's unique id. + */ + async destroy(socket) { + + if (!hasSocket(socket)) + return; + + await this.disconnect(socket); + + sockets.delete(socket); + }, + + /** + * Add a override for an certificate error. + * + * @param {string} host + * the server's host name. + * @param {string} port + * the server's port. + * @param {int[]} rawDER + * the certificate which should be overridden as byte array. + * @param {int} flags + * the override flags. + */ + async addCertErrorOverride(host, port, rawDER, flags) { + + const overrideService = Cc["@mozilla.org/security/certoverride;1"] + .getService(Ci.nsICertOverrideService); + + const certDB = Cc["@mozilla.org/security/x509certdb;1"] + .getService(Ci.nsIX509CertDB); + + // The constructX509 has an incompatible change. + // While newer version consume an array, older version use strings. + // This magic is only needed for thunderbird 68 + let cert = null; + try { + // Try first with the new api.. + cert = certDB.constructX509(rawDER); + } catch (ex) { + + if (ex.name !== "NS_ERROR_FAILURE") + throw ex; + + // ... other wise the use the old api. + cert = ""; + for (const ch of rawDER) + cert += (String.fromCharCode(ch)); + + cert = certDB.constructX509(cert); + } + + overrideService.rememberValidityOverride( + host, port, cert, flags, false); + }, + + // Event handlers... + /** + * The OnData handler is called whenever data is received. + */ + onData: new ExtensionCommon.EventManager({ + context, + name: "sieve.socket.onData", + register: (fire, socket) => { + + const callback = async (bytes) => { + return await fire.async(bytes); + }; + + getSocket(socket).on("data", callback); + + return () => { + if (hasSocket(socket)) + getSocket(socket).on("data"); + }; + } + }).api(), + + /** + * The OnError handler is called whenever an error occurred. + * The close event will be called directly after. + */ + onError: new ExtensionCommon.EventManager({ + context, + name: "sieve.socket.onError", + register: (fire, socket) => { + + const callback = async (error) => { + return await fire.async(error); + }; + + getSocket(socket).on("error", callback); + + return () => { + if (hasSocket(socket)) + getSocket(socket).on("error"); + }; + } + }).api(), + + /** + * The OnClose handler is called whenever the connection to the + * remote server was closed. + */ + onClose: new ExtensionCommon.EventManager({ + context, + name: "sieve.socket.onClose", + register: (fire, socket) => { + + const callback = async () => { + return await fire.async(); + }; + + getSocket(socket).on("close", callback); + + return () => { + if (hasSocket(socket)) + getSocket(socket).on("close"); + }; + } + }).api() + + } + } + }; + } + } + + exports.SieveSocketApi = SieveSocketApi; + +})(this); diff --git a/src/wx/api/sieve/SieveSocketApi.json b/src/wx/api/sieve/SieveSocketApi.json new file mode 100644 index 00000000..8101d48d --- /dev/null +++ b/src/wx/api/sieve/SieveSocketApi.json @@ -0,0 +1,213 @@ +[ + { + "namespace": "sieve.socket", + "description": "Sieve Socket Connection", + "functions": [ + { + "name": "create", + "type": "function", + "description": "Creates (but does not connect) a server connection", + "async": true, + "parameters": [ + { + "name": "host", + "type": "string", + "description": "The remote's host name" + }, + { + "name": "port", + "type": "string", + "description": "The remote's port" + }, + { + "name": "level", + "type": "integer", + "description": "The log level to be used" + } + ] + }, + { + "name": "connect", + "type": "function", + "description": "Connects to a remote server.", + "async": true, + "parameters": [ + { + "name": "id", + "type": "string", + "description": "The unique socket handle" + } + ] + }, + { + "name": "startTLS", + "type": "function", + "description": "Upgrades the socket to be secure. In case the upgrade fails the socket will be closed", + "async": true, + "parameters": [ + { + "name": "id", + "description": "The unique socket handle", + "type": "string" + } + ] + }, + { + "name": "send", + "type": "function", + "description": "Upgrades the socket to be secure. In case the upgrade fails the socket will be closed", + "async": true, + "parameters": [ + { + "name": "id", + "description": "The unique socket handle", + "type": "string" + }, + { + "name": "bytes", + "description": "The unique socket handle", + "type": "array", + "items": { + "type": "integer" + } + } + ] + }, + { + "name": "isAlive", + "type": "function", + "description": "Checks if the socket is still alive and connected", + "async": true, + "parameters": [ + { + "name": "id", + "description": "The unique socket handle", + "type": "string" + } + ] + }, + { + "name": "disconnect", + "type": "function", + "description": "Disconnects from the remote server.", + "async": true, + "parameters": [ + { + "name": "id", + "type": "string", + "description": "the connection id" + } + ] + }, + { + "name": "destroy", + "type": "function", + "description": "Destroys the given session", + "async": true, + "parameters": [ + { + "name": "id", + "type": "string", + "description": "The sessions unique id" + } + ] + }, + { + "name": "addCertErrorOverride", + "type": "function", + "description": "Adds a certificate error override for the given cert.", + "async": true, + "parameters": [ + { + "name": "host", + "type": "string", + "description": "The hostname to override" + }, + { + "name": "port", + "type": "string", + "description": "The port to override" + }, + { + "name": "rawDER", + "type": "array", + "description": "The server's certificate", + "items": { + "type": "integer" + } + }, + { + "name": "flags", + "type": "integer", + "description": "The override flags." + } + ] + } + ], + "events": [ + { + "name": "onData", + "type": "function", + "description": "Called when data is received", + "parameters": [ + { + "name": "bytes", + "description": "The data received as byte array.", + "type": "array", + "items": { + "type": "integer" + } + } + ], + "returns": {}, + "extraParameters": [ + { + "name": "socket", + "description": "The socket's unique id", + "type": "string" + } + ] + }, + { + "name": "onError", + "type": "function", + "description": "Called whenever the socket was closed unexpectedly", + "parameters": [ + { + "name": "error", + "type": "object", + "description": "An object containing details about the error." + } + ], + "returns": {}, + "extraParameters": [ + { + "name": "id", + "description": "The socket's unique id", + "type": "string" + } + ] + }, + { + "name": "onClose", + "type": "function", + "description": "Called after the socket was closed.", + "parameters": [ + { + "name": "hadError", + "description": "True in case the socket was closed due to an error", + "type": "boolean" + } + ], + "returns": {}, + "extraParameters": [ + { + "name": "id", + "description": "The socket's unique id", + "type": "string" + } + ] + } + ] + } +]
\ No newline at end of file diff --git a/src/wx/background.html b/src/wx/background.html index 1370dc5c..3c2d76e6 100644 --- a/src/wx/background.html +++ b/src/wx/background.html @@ -2,32 +2,6 @@ <html lang="en"> <head> <meta charset="utf-8"> - <script src="./libs/managesieve.ui/utils/SieveFakeRequire.js"></script> - - <script src="./libs/managesieve.ui/utils/SieveLogger.js"></script> - - <script src="./libs/managesieve.ui/utils/SieveUniqueId.js"></script> - <script src="./libs/managesieve.ui/utils/SieveAbstractIpcClient.js"></script> - <script src="./libs/managesieve.ui/utils/SieveIpcClient.js"></script> - - <script src="./libs/managesieve.ui/settings/logic/SieveAbstractPrefManager.js"></script> - <script src="./libs/managesieve.ui/settings/logic/SievePrefManager.js"></script> - - <script src="./libs/managesieve.ui/settings/logic/SieveAbstractMechanism.js"></script> - <script src="./libs/managesieve.ui/settings/logic/SieveAbstractAuthorization.js"></script> - <script src="./libs/managesieve.ui/settings/logic/SieveAuthorization.js"></script> - <script src="./libs/managesieve.ui/settings/logic/SieveAbstractAuthentication.js"></script> - <script src="./libs/managesieve.ui/settings/logic/SieveAuthentication.js"></script> - <script src="./libs/managesieve.ui/settings/logic/SieveSecurity.js"></script> - <script src="./libs/managesieve.ui/settings/logic/SieveAbstractHost.js"></script> - <script src="./libs/managesieve.ui/settings/logic/SieveHost.js"></script> - <script src="./libs/managesieve.ui/settings/logic/SieveEditorSettings.js"></script> - <script src="./libs/managesieve.ui/settings/logic/SieveAccountSettings.js"></script> - - <script src="./libs/managesieve.ui/settings/logic/SieveAbstractAccount.js"></script> - <script src="./libs/managesieve.ui/settings/logic/SieveAbstractAccounts.js"></script> - <script src="./libs/managesieve.ui/settings/logic/SieveAccounts.js"></script> - - <script src="./background.js"></script> + <script type="module" src="./background.mjs"></script> </head> </html>
\ No newline at end of file diff --git a/src/wx/background.js b/src/wx/background.mjs index 0d0e642d..a8495910 100644 --- a/src/wx/background.js +++ b/src/wx/background.mjs @@ -9,14 +9,17 @@ * Thomas Schmid <schmid-thomas@gmx.net> */ -(async function () { +/* global browser */ + +import { SieveSession } from "./libs/libManageSieve/SieveSession.mjs"; +import { SieveCertValidationException } from "./libs/libManageSieve/SieveExceptions.mjs"; - "use strict"; +import { SieveLogger } from "./libs/managesieve.ui/utils/SieveLogger.mjs"; +import { SieveIpcClient } from "./libs/managesieve.ui/utils/SieveIpcClient.mjs"; +import { SieveAccounts } from "./libs/managesieve.ui/settings/logic/SieveAccounts.mjs"; - /* global browser */ - /* global SieveIpcClient */ - /* global SieveAccounts */ - /* global SieveLogger */ + +(async function () { const ERROR_UNTRUSTED = 1; const ERROR_MISMATCH = 2; @@ -28,6 +31,8 @@ const accounts = await (new SieveAccounts().load()); + const sessions = new Map(); + // TODO Extract into separate class.. /** * Gets a tab by its script and account name. @@ -74,8 +79,13 @@ if (tabs.length) return; - for (const id of accounts.getAccountIds()) - browser.sieve.session.destroy(id); + for (const id of accounts.getAccountIds()) { + if (!sessions.has(id)) + continue; + + await (sessions.get(id).disconnect(true)); + sessions.delete(id); + } }); // ------------------------------------------------------------------------ // @@ -184,7 +194,11 @@ logger.logAction(`Is connected ${msg.payload.account}`); const id = msg.payload.account; - return browser.sieve.session.isConnected(id); + + if (!sessions.has(id)) + return false; + + return sessions.get(id).isConnected(); }, "account-connect": async function (msg) { @@ -228,57 +242,81 @@ return await authorization.getAuthorization(); }; - const onCertError = async (secInfo) => { - logger.logAction(`Certificate Error for ${secInfo.host}:${secInfo.port}`); + if (sessions.has(id)) + throw new Error("Id already in use"); - const rv = await SieveIpcClient.sendMessage( - "accounts", "script-show-certerror", secInfo); + sessions.set(id, + new SieveSession(id, options)); - if (!rv) - return; + sessions.get(id).on("authenticate", + async (hasPassword) => { return await onAuthenticate(hasPassword); }); + sessions.get(id).on("authorize", + async () => { return await onAuthorize(); }); - let overrideBits = 0; - if (secInfo.isNotValidAtThisTime) - overrideBits |= ERROR_TIME; + const hostname = await host.getHostname(); + const port = await host.getPort(); - if (secInfo.isUntrusted) - overrideBits |= ERROR_UNTRUSTED; + try { + await sessions.get(id).connect(hostname, `${port}`); + } catch (ex) { - if (secInfo.isDomainMismatch) - overrideBits |= ERROR_MISMATCH; + await (actions["account-disconnect"](msg)); - await browser.sieve.session.addCertErrorOverride( - secInfo.host, secInfo.port, secInfo.rawDER, overrideBits); - }; + if (ex instanceof SieveCertValidationException) { + const secInfo = ex.getSecurityInfo(); - await browser.sieve.session.create(id, options); - await browser.sieve.session.onAuthenticate.addListener( - async (hasPassword) => { return await onAuthenticate(hasPassword); }, id); - await browser.sieve.session.onAuthorize.addListener( - async () => { return await onAuthorize(); }, id); + const rv = (await SieveIpcClient.sendMessage( + "accounts", "account-show-certerror", secInfo)); - await browser.sieve.session.onCertError.addListener( - async (secInfo) => { return await onCertError(secInfo); }, id); + // Dialog was canceled we bail out. + if (!rv) + return; - const hostname = await host.getHostname(); - const port = await host.getPort(); + let overrideBits = 0; + if (secInfo.isNotValidAtThisTime) + overrideBits |= ERROR_TIME; + + if (secInfo.isUntrusted) + overrideBits |= ERROR_UNTRUSTED; + + if (secInfo.isDomainMismatch) + overrideBits |= ERROR_MISMATCH; + + await (browser.sieve.socket.addCertErrorOverride( + secInfo.host, `${secInfo.port}`, secInfo.rawDER, overrideBits)); + + await (actions["account-connect"](msg)); + return; + } + + // connecting failed for some reason, which means we + // need to handle the error. + logger.logAction("Connecting failed due to an error " + ex); - await browser.sieve.session.connect(id, hostname, `${port}`); + await SieveIpcClient.sendMessage( + "accounts", "account-show-error", ex.message); + } }, "account-disconnect": async function (msg) { logger.logAction(`Disconnect ${msg.payload.account}`); - await browser.sieve.session.destroy(msg.payload.account); + + const id = msg.payload.account; + if (!sessions.has(id)) + return; + + await (sessions.get(id).disconnect(msg.payload.account)); + sessions.delete(id); }, "account-list": async function (msg) { logger.logAction(`List scripts for ${msg.payload.account}`); - return await browser.sieve.session.listScripts(msg.payload.account); + return await sessions.get(msg.payload.account).listScripts(); }, "account-capabilities": async function (msg) { logger.logAction(`Get capabilities for ${msg.payload.account}`); - return await browser.sieve.session.capabilities(msg.payload.account); + return await sessions.get(msg.payload.account).capabilities(); }, // Script endpoint... @@ -290,7 +328,7 @@ const name = await SieveIpcClient.sendMessage("accounts", "script-show-create", account); if (name.trim() !== "") - await browser.sieve.session.putScript(account, name, "#test\r\n"); + await sessions.get(account).putScript(name, "#test\r\n"); return name; }, @@ -311,7 +349,7 @@ if (newName === oldName) return false; - await browser.sieve.session.renameScript(account, oldName, newName); + await sessions.get(account).renameScript(oldName, newName); return true; }, @@ -329,7 +367,7 @@ const rv = await SieveIpcClient.sendMessage("accounts", "script-show-delete", name); if (rv === true) - await browser.sieve.session.deleteScript(account, name); + await sessions.get(account).deleteScript(name); return rv; }, @@ -340,7 +378,7 @@ logger.logAction(`Activate ${name} for ${account}`); - await browser.sieve.session.activateScript(account, name); + await sessions.get(account).activateScript(name); }, "script-deactivate": async function (msg) { @@ -348,7 +386,7 @@ logger.logAction(`Deactivate script for ${account}`); - await browser.sieve.session.activateScript(account); + await sessions.get(account).activateScript(); }, "script-edit": async function (msg) { @@ -382,7 +420,7 @@ logger.logAction(`Get ${name} for ${account}`); - return await browser.sieve.session.getScript(account, name); + return await sessions.get(account).getScript(name); }, "script-check": async function (msg) { @@ -391,7 +429,14 @@ logger.logAction(`Check Script for ${account}`); - return await browser.sieve.session.checkScript(account, script); + try { + await sessions.get(account).checkScript(script); + } catch (ex) { + // FIXME We need to rethrow incase checkscript returns a SieveServerException. + return ex.getResponse().getMessage(); + } + + return ""; }, "script-save": async function (msg) { @@ -401,7 +446,7 @@ logger.logAction(`Save ${name} for ${account}`); - await browser.sieve.session.putScript(account, name, script); + await sessions.get(account).putScript(name, script); }, "account-get-settings": async function (msg) { @@ -467,7 +512,7 @@ return value; }, - "get-default-preference": async(msg) => { + "get-default-preference": async (msg) => { const name = msg.payload.data; logger.logAction(`Get default value for ${name}`); @@ -485,7 +530,7 @@ await accounts.getAccountById(account).getEditor().setValue(name, value); }, - "set-default-preference": async(msg) => { + "set-default-preference": async (msg) => { const name = msg.payload.key; const value = msg.payload.value; diff --git a/src/wx/libs/libManageSieve/SieveBase64.mjs b/src/wx/libs/libManageSieve/SieveBase64.mjs new file mode 100644 index 00000000..015c1171 --- /dev/null +++ b/src/wx/libs/libManageSieve/SieveBase64.mjs @@ -0,0 +1,356 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +import { + SieveAbstractBase64Decoder, + SieveAbstractBase64Encoder +} from "./SieveAbstractBase64.mjs"; + +const DECODED_QUANTUM = 3; +const ENCODED_QUANTUM = 4; + +const CHAR_PADDING = 61; + +// First Quantum +const Q1_HIGH_OFFSET = 0; +const Q1_HIGH_MASK = 0x3F; +const Q1_HIGH_SHIFT = 2; + +const Q1_LOW_OFFSET = 1; +const Q1_LOW_MASK = 0x30; +const Q1_LOW_SHIFT = 4; + +// Second Quantum +const Q2_HIGH_OFFSET = 1; +const Q2_HIGH_MASK = 0x0F; +const Q2_HIGH_SHIFT = 4; + +const Q2_LOW_OFFSET = 2; +const Q2_LOW_MASK = 0x3C; +const Q2_LOW_SHIFT = 2; + +// Third Quantum +const Q3_HIGH_OFFSET = 2; +const Q3_HIGH_MASK = 0x03; +const Q3_HIGH_SHIFT = 6; + +const Q3_LOW_OFFSET = 3; +const Q3_LOW_MASK = 0x3F; +const Q3_LOW_SHIFT = 0; + +const TWO_QUANTUM = 2; +const ONE_QUANTUM = 1; + +// Byte Offsets +const FIRST_BYTE = 0; +const SECOND_BYTE = 1; +const THIRD_BYTE = 2; +const FOURTH_BYTE = 3; + +const BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +/** + * + */ +class SieveWebBase64Encoder extends SieveAbstractBase64Encoder { + + /** + * @inheritdoc + */ + constructor(decoded) { + + if (Array.isArray(decoded)) + decoded = new Uint8Array(decoded); + + if (!(decoded instanceof Uint8Array)) + decoded = (new TextEncoder()).encode(decoded); + + super(decoded); + } + + /** + * Converts the 6bit integer into the corresponding base64 bytes + * @param {byte} data + * the 6 bit integer to be converted + * @returns {string} + * the corresponding base64 character + * + */ + lookup(data) { + return (BASE64_CHARS[data].charCodeAt()); + } + + /** + * Encodes the first byte. It is exclusively used by the first + * quantum. + * + * @param {byte} q1 + * the first quantum to be encoded. + * @returns {byte} + * the encoded quantum + */ + encodeFirstByte(q1) { + return this.lookup((q1 >> Q1_HIGH_SHIFT) & Q1_HIGH_MASK); + } + + /** + * Encodes the second byte. This byte is shared by the first + * and second quantum. + * + * The second quantum is optional and should be omitted or + * set to zero in case the end of the decoded data is reached. + * + * @param {byte} q1 + * the first quantum. + * @param {byte} [q2] + * the second quantum. + * @returns {byte} + * the encoded quantum. + */ + encodeSecondByte(q1, q2) { + if ((typeof(q2) === "undefined") || (q2 === null)) + return this.lookup((q1 << Q1_LOW_SHIFT) & Q1_LOW_MASK); + + return this.lookup( + ((q1 << Q1_LOW_SHIFT) & Q1_LOW_MASK) + + ((q2 >> Q2_HIGH_SHIFT) & Q2_HIGH_MASK)); + } + + /** + * Encodes the third byte. This byte is shared by the second + * and third quantum. + * + * The third quantum is optional and should be omitted or + * set to zero in case the end of the decoded date is reached. + * + * @param {byte} q2 + * the second quantum. + * @param {byte} [q3] + * the third quantum. + * @returns {byte} + * the encoded quantum. + */ + encodeThirdByte(q2, q3) { + if ((typeof(q3) === "undefined") || (q3 === null)) + return this.lookup(((q2 << Q2_LOW_SHIFT) & Q2_LOW_MASK)); + + return this.lookup( + ((q2 << Q2_LOW_SHIFT) & Q2_LOW_MASK) + + ((q3 >> Q3_HIGH_SHIFT) & Q3_HIGH_MASK)); + } + + /** + * Encodes the fourth byte. This byte is exclusively used by + * the fourth quantum. + * + * @param {byte} q4 + * the fourth quantum. + * @returns {byte} + * the encoded quantum. + */ + encodeFourthByte(q4) { + return this.lookup((q4 >> Q3_LOW_SHIFT) & Q3_LOW_MASK); + } + + /** + * @inheritdoc + */ + toArray() { + + const decoded = this.decoded; + + const data = new Uint8Array( + Math.ceil(decoded.byteLength / DECODED_QUANTUM) * ENCODED_QUANTUM); + + const padding = (decoded.length % DECODED_QUANTUM); + + for (let i = 0; i < decoded.length - padding; i += DECODED_QUANTUM) { + + const offset = (i / DECODED_QUANTUM) * ENCODED_QUANTUM; + + data[offset + FIRST_BYTE] = this.encodeFirstByte(decoded[i + FIRST_BYTE]); + data[offset + SECOND_BYTE] + = this.encodeSecondByte(decoded[i + FIRST_BYTE], decoded[i + SECOND_BYTE]); + data[offset + THIRD_BYTE] + = this.encodeThirdByte(decoded[i + SECOND_BYTE], decoded[i + THIRD_BYTE]); + data[offset + FOURTH_BYTE] = this.encodeFourthByte(decoded[i + THIRD_BYTE]); + } + + if (padding === ONE_QUANTUM) { + const offset = data.length - ENCODED_QUANTUM; + + data[offset + FIRST_BYTE] = this.encodeFirstByte(decoded[decoded.length - 1]); + data[offset + SECOND_BYTE] = this.encodeSecondByte(decoded[decoded.length - 1]); + data[offset + THIRD_BYTE] = CHAR_PADDING; + data[offset + FOURTH_BYTE] = CHAR_PADDING; + } + + if (padding === TWO_QUANTUM) { + const offset = data.length - ENCODED_QUANTUM; + + data[offset + FIRST_BYTE] = this.encodeFirstByte( + decoded[decoded.length - 2]); + data[offset + SECOND_BYTE] = this.encodeSecondByte( + decoded[decoded.length - 2], decoded[decoded.length - 1]); + data[offset + THIRD_BYTE] = this.encodeThirdByte( + decoded[decoded.length - 1]); + data[offset + FOURTH_BYTE] = CHAR_PADDING; + } + + return data; + } + + /** + * @inheritdoc + */ + toUtf8() { + return (new TextDecoder("UTF-8")).decode(this.toArray()); + } +} + +/** + * Implements a base64 decoded as atob is not capable of + * decoding an encoded UTF8 string. + */ +class SieveWebBase64Decoder extends SieveAbstractBase64Decoder { + + /** + * @inheritdoc + */ + constructor(encoded) { + + if (!(encoded instanceof Uint8Array)) + encoded = new Uint8Array((new TextEncoder()).encode(encoded)); + + super(encoded); + } + + /** + * Calculates the decoded length. + * @param {Uint8Array} encoded + * the encoded data which should be analyzed + * @returns {int} + * the decoded length in bytes + */ + calculateLength(encoded) { + const length = Math.floor((encoded.length / ENCODED_QUANTUM) * DECODED_QUANTUM); + + // If the last character is not equals to "=" then we know + // the last quantum is fully or empty. + if (encoded[encoded.length - 1] !== CHAR_PADDING) + return length; + + // In case the last two bytes are not equal to "=" we know + // we know the last quantum is 16bit. + if (encoded[encoded.length - 2] !== CHAR_PADDING) + return length - ONE_QUANTUM; + + // Otherwise it is a single "=" at the end which means the + // las quantum is 8 bits. + return length - TWO_QUANTUM; + } + + /** + * Decodes the first byte or quantum + * + * @param {int} offset + * the offset which points to the first group of the encoded data. + * @param {Uint8Array} encoded + * the encoded data. + * @returns {byte} + * The first byte or quantum. + */ + decodeFirstQuantum(offset, encoded) { + return ((this.lookup(encoded[offset + Q1_HIGH_OFFSET]) & Q1_HIGH_MASK) << Q1_HIGH_SHIFT) + + ((this.lookup(encoded[offset + Q1_LOW_OFFSET]) & Q1_LOW_MASK) >> Q1_LOW_SHIFT); + } + + /** + * Decodes the second byte or quantum + * + * @param {int} offset + * the offset which points to the first group of the encoded data. + * @param {Uint8Array} encoded + * the encoded data. + * @returns {byte} + * The second byte or quantum. + */ + decodeSecondQuantum(offset, encoded) { + return ((this.lookup(encoded[offset + Q2_HIGH_OFFSET]) & Q2_HIGH_MASK) << Q2_HIGH_SHIFT) + + ((this.lookup(encoded[offset + Q2_LOW_OFFSET]) & Q2_LOW_MASK) >> Q2_LOW_SHIFT); + } + + /** + * Decodes the third byte or quantum + * + * @param {int} offset + * the offset which points to the first group of the encoded data. + * @param {Uint8Array} encoded + * the encoded data. + * @returns {byte} + * The third byte or quantum. + */ + decodeThirdQuantum(offset, encoded) { + + return ((this.lookup(encoded[offset + Q3_HIGH_OFFSET]) & Q3_HIGH_MASK) << Q3_HIGH_SHIFT) + + ((this.lookup(encoded[offset + Q3_LOW_OFFSET]) & Q3_LOW_MASK) << Q3_LOW_SHIFT); + } + + /** + * Converts a the base64 code point value to the decoded value. + * @param {byte} data + * the base64 code point + * @returns {byte} + * the decoded value. + */ + lookup(data) { + return BASE64_CHARS.indexOf(String.fromCharCode(data)); + } + + /** + * @inheritdoc + */ + toArray() { + + const encoded = this.encoded; + // adjust the array view to the padding + + const data = new Uint8Array(this.calculateLength(encoded)); + + for (let i = 0; i < data.length - 1; i += DECODED_QUANTUM) { + const offset = (i / DECODED_QUANTUM) * ENCODED_QUANTUM; + + data[i + FIRST_BYTE] = this.decodeFirstQuantum(offset, encoded); + data[i + SECOND_BYTE] = this.decodeSecondQuantum(offset, encoded); + data[i + THIRD_BYTE] = this.decodeThirdQuantum(offset, encoded); + } + + const padding = (data.length % DECODED_QUANTUM); + + if (padding === TWO_QUANTUM) { + const offset = ((data.length - padding) / DECODED_QUANTUM) * ENCODED_QUANTUM; + data[data.length - 2] = this.decodeFirstQuantum(offset, encoded); + data[data.length - 1] = this.decodeSecondQuantum(offset, encoded); + } + + if (padding === ONE_QUANTUM) { + const offset = ((data.length - padding) / DECODED_QUANTUM) * ENCODED_QUANTUM; + data[data.length - 1] = this.decodeFirstQuantum(offset, encoded); + } + + return data; + } +} + +export { + SieveWebBase64Decoder as SieveBase64Decoder, + SieveWebBase64Encoder as SieveBase64Encoder +}; diff --git a/src/wx/libs/libManageSieve/SieveClient.js b/src/wx/libs/libManageSieve/SieveClient.js deleted file mode 100644 index 22b563b5..00000000 --- a/src/wx/libs/libManageSieve/SieveClient.js +++ /dev/null @@ -1,539 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - /* global Components */ - - const Cc = Components.classes; - const Ci = Components.interfaces; - // const Cu = Components.utils; - const Cr = Components.results; - - // Handle all imports.. - const { SieveAbstractClient } = require("./SieveAbstractClient.js"); - const { SieveResponseParser } = require("./SieveResponseParser.js"); - const { SieveRequestBuilder } = require("./SieveRequestBuilder.js"); - - const { SieveCertValidationException } = require("./SieveExceptions.js"); - - const NEW_TRANSPORT_API = 4; - const OLD_TRANSPORT_API = 5; - - // Input & output stream constants. - const DEFAULT_FLAGS = 0; - const DEFAULT_SEGMENT_SIZE = 0; - const DEFAULT_SEGMENT_COUNT = 0; - - const SEGMENT_SIZE = 5000; - const SEGMENT_COUNT = 2; - - const TRANSPORT_INSECURE = 0; - const TRANSPORT_SECURE = 1; - - /** - * This realizes the abstract sieve implementation by using - * the mozilla specific network implementation. - */ - class SieveMozClient extends SieveAbstractClient { - - - /** - * Creates a new instance - * @param {SieveAbstractLogger} logger - * the logger which should be used. - */ - constructor(logger) { - - super(); - - this.timeoutTimer = null; - this.idleTimer = null; - - this.outstream = null; - this.binaryOutStream = null; - - this._logger = logger; - this.secure = true; - } - - - /** - * Implements the Gecko Component Manager's interfaces. - * So that the JavaScript code can be passed as interface - * to C++. - * - * In case the interface is not supported an NS_ERROR_NO_INTERFACE - * will be thrown. - * - * @param {nsIIDRef} uuid - * the interface's unique id to check. - * - * @returns {SieveMozClient} - * a self reference. - */ - QueryInterface(uuid) { - if (uuid.equals(Ci.nsISupports)) - return this; - // onProxyAvailable... - if (uuid.equals(Ci.nsIProtocolProxyCallback)) - return this; - // onDataAvailable... - if (uuid.equals(Ci.nsIStreamListener)) - return this; - // onStartRequest and onStopRequest... - if (uuid.equals(Ci.nsIRequestObserver)) - return this; - - throw Cr.NS_ERROR_NO_INTERFACE; - } - - /** - * @inheritdoc - */ - isAlive() { - if (!super.isAlive(this)) - return false; - - return this.socket.isAlive(); - } - - /** - * This method secures the connection to the sieve server. By activating - * Transport Layer Security all Data exchanged is encrypted. - * - * Before calling this method you need to request a encrypted connection by - * sending a startTLSRequest. Invoke this method immediately after the server - * confirms switching to TLS. - * - **/ - async startTLS() { - - await super.startTLS(); - - const securityInfo = this.socket.securityInfo.QueryInterface(Ci.nsISSLSocketControl); - securityInfo.StartTLS(); - } - - /** - * @inheritdoc - */ - onStartTimeout() { - - // clear any existing timeouts - if (this.timeoutTimer) - this.timeoutTimer.cancel(); - - // ensure the idle timer is stopped - this.onStopIdle(); - - // then restart the timeout timer. - if (this.timeoutTimer) - this.timeoutTimer.initWithCallback( - this, this.getTimeoutWait(), - Components.interfaces.nsITimer.TYPE_ONE_SHOT); - - } - - /** - * @inheritdoc - */ - onStopTimeout() { - - // clear any existing timeouts. - if (this.timeoutTimer) - this.timeoutTimer.cancel(); - - // and start the idle timer - this.onStartIdle(); - } - - /** - * Called when the idle timer should be started or restarted - */ - onStartIdle() { - - // first ensure the timer is stopped.. - this.onStopIdle(); - - // ... then configure the timer. - const delay = this.getIdleWait(); - - if (!delay) - return; - - if (!this.idleTimer) - return; - - this.idleTimer.initWithCallback(this, delay, - Components.interfaces.nsITimer.TYPE_ONE_SHOT); - } - - /** - * Called when the idle timer should be stopped. - */ - onStopIdle() { - - if (!this.idleTimer) - return; - - this.idleTimer.cancel(); - } - - /** - * When a mozilla timer triggers, it calls this - * well known method. - * - * @param {nsITimer} timer - * the timer which caused this callback. - * - */ - notify(timer) { - - if (this.idleTimer === timer) { - (async () => { await this.onIdle(); })(); - return; - } - - if (this.timeoutTimer === timer) { - (async () => { await this.onTimeout(); })(); - return; - } - } - - /** - * @inheritdoc - */ - createParser(data) { - return new SieveResponseParser(data); - } - - /** - * @inheritdoc - */ - createRequestBuilder() { - return new SieveRequestBuilder(); - } - - /** - * @inheritdoc - */ - getLogger() { - return this._logger; - } - - /** - * @inheritdoc - */ - isSecure() { - return this.secure; - } - - /** - * @inheritdoc - */ - connect(host, port, secure, proxy) { - if (this.socket) - return this; - - /* if ( (this.socket != null) && (this.socket.isAlive()) ) - return;*/ - - this.host = host; - this.port = port; - this.secure = secure; - - this.idleTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - this.timeoutTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - - this.getLogger().logState(`Connecting to ${this.host}:${this.port} ...`); - - // If we know the proxy setting, we can do a shortcut... - if (proxy) { - this.onProxyAvailable(null, null, proxy[0], null); - return this; - } - - this.getLogger().logState(`Lookup Proxy Configuration for x-sieve://${this.host}:${this.port} ...`); - - const ios = Cc["@mozilla.org/network/io-service;1"] - .getService(Ci.nsIIOService); - - // const uri = ios.newURI("x-sieve://" + this.host + ":" + this.port, null, null); - const uri = ios.newURI(`http://${this.host}:${this.port}`, null, null); - - this.getLogger().logState(`Connecting to ${uri.hostPort}:${this.host} ...`); - - const pps = Cc["@mozilla.org/network/protocol-proxy-service;1"] - .getService(Ci.nsIProtocolProxyService); - pps.asyncResolve(uri, 0, this); - - return this; - } - - /** - * We need this wrapper for compatibility. Stating Thunderbird 69 - * Bug 1558726 (https://bugzilla.mozilla.org/show_bug.cgi?id=1558726) - * introduced a breaking change for this interface. - * - * Before the change the first parameter was an array and the second one - * the array length. After the change the length parameter was removed - * which causes all the other arguments to shift by one. - * - * @private - * @param {string} host - * the host name to which to connect - * @param {int} port - * the host's port - * @param {boolean} secure - * if true a secure socket is created. - * @param {*} [proxyInfo] - * optional proxy information. - * - * @returns {nsISocketTransportService} - * the transport service or throws an exception in case creation failed. - */ - createTransport(host, port, secure, proxyInfo) { - - const transportService = - Cc["@mozilla.org/network/socket-transport-service;1"] - .getService(Ci.nsISocketTransportService); - - if (transportService.createTransport.length === NEW_TRANSPORT_API) - return transportService.createTransport(((secure) ? ["starttls"] : []), host, port, proxyInfo); - - if (transportService.createTransport.length === OLD_TRANSPORT_API) { - if (secure) - return transportService.createTransport(["starttls"], TRANSPORT_SECURE, host, port, proxyInfo); - - return transportService.createTransport(null, TRANSPORT_INSECURE, host, port, proxyInfo); - } - - throw new Error("Unknown Create Transport signature"); - } - - /** - * An async callback for the proxy lookup. - * - * @private - * @param {nsIRequest} request - * thr request for which the proxy information as requested. - * @param {*} aURI - * @param {*} aProxyInfo - * the proxy information from the lookup, if null a direct - * connection will be used. - * @param {*} status - * - **/ - // eslint-disable-next-line no-unused-vars - onProxyAvailable( request, aURI, aProxyInfo, status) { - - if (aProxyInfo) - this.getLogger().logState(`Using Proxy: [${aProxyInfo.type}] ${aProxyInfo.host}:${aProxyInfo.port}`); - else - this.getLogger().logState("Using Proxy: Direct"); - - - this.socket = this.createTransport(this.host, this.port, this.isSecure(), aProxyInfo); - - this.outstream = this.socket.openOutputStream( - DEFAULT_FLAGS, DEFAULT_SEGMENT_SIZE, DEFAULT_SEGMENT_COUNT); - - this.binaryOutStream = - Cc["@mozilla.org/binaryoutputstream;1"] - .createInstance(Ci.nsIBinaryOutputStream); - - this.binaryOutStream.setOutputStream(this.outstream); - - const stream = this.socket.openInputStream( - DEFAULT_FLAGS, DEFAULT_SEGMENT_SIZE, DEFAULT_SEGMENT_COUNT); - - const pump = Cc["@mozilla.org/network/input-stream-pump;1"] - .createInstance(Ci.nsIInputStreamPump); - - pump.init(stream, SEGMENT_SIZE, SEGMENT_COUNT, true); - - pump.asyncRead(this, null); - } - - /** - * @inheritdoc - */ - disconnect(reason) { - super.disconnect(reason); - - if (!this.socket) - return; - - this.binaryOutStream.close(); - this.outstream.close(); - this.socket.close(0); - - this.binaryOutStream = null; - this.outstream = null; - this.socket = null; - - this.idleTimer = null; - this.timeoutTimer = null; - - this.getLogger().logState("Disconnected ..."); - } - - /** - * Checks if the status indicates a certificate error. - * - * @param {int} status - * the status which should be checked. - * - * @returns {boolean} - * true in case of a certificate error otherwise false. - */ - isBadCert(status) { - if (status === Cr.NS_OK) - return false; - - this.getLogger().logState("Checking for certificate errors... "); - - const nssErrorsService = Cc["@mozilla.org/nss_errors_service;1"] - .getService(Ci.nsINSSErrorsService); - - try { - const errorType = nssErrorsService.getErrorClass(status); - - if (errorType === Ci.nsINSSErrorsService.ERROR_CLASS_BAD_CERT) { - this.getLogger().logState(`... certificated considered bad (${status},${errorType})`); - return true; - } - - } catch (e) { - console.warn(e); - // nsINSSErrorsService.getErrorClass throws if given a non-TLS, non-cert error, so ignore this - } - - return false; - } - - /** - * Called whenever the connection is terminated. And no more communication - * via this socket is possible. - * - * If the disconnect is planned/"graceful", the error code is zero. - * - * Otherwise the status indicates what caused this disconnect. - * Common issues are link loss (e.g. by switching to offline mode, when - * the network cable is disconnected or when the server closed the connection.)^ - * - * But the socket may be also closed because of non trivial errors. E.g - * in case a tls upgrade failed due to an certification validation error. - * - * @param {nsIRequest} request - * the request which was stopped. - * @param {int} status - * the reason why the stop was called. - */ - // eslint-disable-next-line no-unused-vars - onStopRequest(request, status) { - - this.getLogger().logState(`Disconnected from ${this.host}:${this.port} with status ${status}`); - - // we can ignore this if we are already disconnected. - if (!this.socket) - return; - - let reason; - - if (this.isBadCert(status)) { - const secInfo = this.socket.securityInfo.QueryInterface(Ci.nsITransportSecurityInfo); - - reason = new SieveCertValidationException({ - "host": this.host, - "port": this.port, - - "rawDER": secInfo.serverCert.getRawDER({}), - "fingerprint" : secInfo.serverCert.sha256Fingerprint, - - "message" : secInfo.errorCodeString, - "isDomainMismatch": secInfo.isDomainMismatch, - "isExtendedValidation": secInfo.isExtendedValidation, - "isNotValidAtThisTime": secInfo.isNotValidAtThisTime, - "isUntrusted": secInfo.isUntrusted - }); - } - - // Stop timeout timer, the connection is gone, so... - // ... it won't help us anymore... - this.disconnect(reason); - - // if the request queue is not empty, - // we should call directly on timeout.. - if ((this.listener) && (this.listener.onDisconnect)) - (async () => { await this.listener.onDisconnect(); })(); - } - - /** - * Called when the connection to the remote as is ready. - * - * @param {nsIRequest} request - * the request which is ready. - */ - // eslint-disable-next-line no-unused-vars - onStartRequest(request) { - this.getLogger().logState(`Connected to ${this.host}:${this.port} ...`); - } - - /** - * Call as soon as data arrives and needs to be processed. - * - * @param {nsIRequest} request - * the request object. - * @param {nsIInputStream} inputStream - * the input stream containing the data chunks - * @param {offset} offset - * the offset from the beginning of the stream. - * @param {int} count - * the maximum number of bytes which can be read in this call. - */ - // eslint-disable-next-line no-unused-vars - onDataAvailable(request, inputStream, offset, count) { - - const binaryInStream = Cc["@mozilla.org/binaryinputstream;1"] - .createInstance(Ci.nsIBinaryInputStream); - - binaryInStream.setInputStream(inputStream); - - const data = binaryInStream.readByteArray(count); - - super.onReceive(data); - } - - /** - * @inheritdoc - */ - onSend(data) { - - // Convert string into an UTF-8 array... - const output = Array.prototype.slice.call( - new Uint8Array(new TextEncoder("UTF-8").encode(data))); - - if (this.getLogger().isLevelStream()) - this.getLogger().logStream(`Client -> Server [Byte Array]:\n${output}`); - - this.binaryOutStream.writeByteArray(output, output.length); - - return; - } - } - - exports.Sieve = SieveMozClient; - -})(module.exports); diff --git a/src/wx/libs/libManageSieve/SieveClient.mjs b/src/wx/libs/libManageSieve/SieveClient.mjs new file mode 100644 index 00000000..af204f90 --- /dev/null +++ b/src/wx/libs/libManageSieve/SieveClient.mjs @@ -0,0 +1,171 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +/* global browser */ + +// Handle all imports.. +import { SieveAbstractClient } from "./SieveAbstractClient.mjs"; + + +import { + SieveCertValidationException, + SieveClientException, + SieveException +} from "./SieveExceptions.mjs"; + +/** + * This realizes the abstract sieve implementation by using + * the mozilla specific network implementation. + */ +class SieveMozClient extends SieveAbstractClient { + + + /** + * Creates a new instance + * @param {SieveLogger} logger + * the logger which should be used. + */ + constructor(logger) { + + super(); + + this._logger = logger; + this.secure = true; + } + + /** + * @inheritdoc + */ + isAlive() { + if (!super.isAlive(this)) + return false; + + return browser.sieve.socket.isAlive(this.socket); + } + + + /** + * This method secures the connection to the sieve server. By activating + * Transport Layer Security all Data exchanged is encrypted. + * + * Before calling this method you need to request a encrypted connection by + * sending a startTLSRequest. Invoke this method immediately after the server + * confirms switching to TLS. + * + **/ + async startTLS() { + await super.startTLS(); + + this.getLogger().logState("[SieveClient:startTLS()] Upgrading to secure socket"); + + await browser.sieve.socket.startTLS(this.socket); + } + + /** + * @inheritdoc + */ + getLogger() { + return this._logger; + } + + /** + * @inheritdoc + */ + isSecure() { + return true; + } + + /** + * @inheritdoc + */ + async connect(host, port) { + if (this.socket) + return this; + + this.host = host; + this.port = port; + + this.getLogger().logState(`Connecting to ${this.host}:${this.port} ...`); + + this.socket = await (browser.sieve.socket.create(host, port, this.getLogger().level())); + + await (browser.sieve.socket.onData.addListener(async (bytes) => { + await super.onReceive(bytes); + }, this.socket)); + + await (browser.sieve.socket.onError.addListener((error) => { + + // Exceptions can't be transferred between experiments and background pages + // This means we need to convert the error object into an exception. + if (error && error.type === "CertValidationError") + error = new SieveCertValidationException(error); + else if (error && error.type === "SocketError") + error = new SieveClientException(error.message); + else + error = new SieveException(`Socket failed without providing an error code.`); + + if ((this.listener) && (this.listener.onError)) + (async () => { await this.listener.onError(error); })(); + }, this.socket)); + + await (browser.sieve.socket.onClose.addListener(() => { + this.disconnect(); + }, this.socket)); + + await (browser.sieve.socket.connect(this.socket)); + + return this; + } + + /** + * @inheritdoc + */ + async disconnect(reason) { + + this.getLogger().logState(`[SieveClient:disconnect] Disconnecting ${this.host}:${this.port}...`); + + await super.disconnect(reason); + + if (!this.socket) { + this.getLogger().logState(`[SieveClient:disconnect()] ... no valid socket`); + return; + } + + this.getLogger().logState(`[SieveClient:disconnect()] ... destroying socket...`); + await browser.sieve.socket.destroy(this.socket); + this.socket = null; + + if ((this.listener) && (this.listener.onDisconnected)) + await this.listener.onDisconnected(); + + this.getLogger().logState("[SieveClient:disconnect()] ... disconnected."); + } + + + /** + * @inheritdoc + */ + onSend(data) { + + // Convert string into an UTF-8 array... + const output = Array.prototype.slice.call( + (new TextEncoder()).encode(data)); + + if (this.getLogger().isLevelStream()) + this.getLogger().logStream(`Client -> Server [Byte Array]:\n${output}`); + + browser.sieve.socket.send(this.socket, output); + + return; + } +} + +export { SieveMozClient as Sieve }; diff --git a/src/wx/libs/libManageSieve/SieveCrypto.js b/src/wx/libs/libManageSieve/SieveCrypto.js deleted file mode 100644 index 9d4cd95d..00000000 --- a/src/wx/libs/libManageSieve/SieveCrypto.js +++ /dev/null @@ -1,126 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - /* global Components */ - - const { SieveAbstractCrypto } = require("./SieveAbstractCrypto.js"); - - /** - * A Mozilla specific crypto implementation. - */ - class SieveMozCrypto extends SieveAbstractCrypto { - - /** - * Returns the HMAC implementation for the given name. - * In case the algorithm is unknown an exception is thrown. - * - * @returns {nsICryptoHMAC} - * the HMAC type. - */ - getCryptoHMAC() { - - if (this.name === "SHA1") - return Components.interfaces.nsICryptoHMAC.SHA1; - - if (this.name === "SHA256") - return Components.interfaces.nsICryptoHMAC.SHA256; - - if (this.name === "MD5") - return Components.interfaces.nsICryptoHMAC.MD5; - - throw Error(`Unknown HMAC algorithm ${this.name}`); - } - - /** - * Returns the hashing implementation for the given name. - * In case the algorithm is unknown an exception is thrown. - * - * @returns {nsICryptoHash} - * the Hash type . - */ - getCryptoHash() { - if (this.name === "SHA1") - return Components.interfaces.nsICryptoHash.SHA1; - - if (this.name === "SHA256") - return Components.interfaces.nsICryptoHash.SHA256; - - if (this.name === "MD5") - return Components.interfaces.nsICryptoHash.MD5; - - throw Error(`Unknown HASH algorithm ${this.name}`); - } - - /** - * @inheritdoc - */ - HMAC(key, bytes, output) { - - if (typeof(key) === "undefined" || key === null) - throw new Error("Invalid key"); - - // Mozilla's api is odd. This means we need some magic here - // The salt has to be a string while the data needs to be an byte array - - if (Array.isArray(bytes) === false) - bytes = this.strToByteArray(bytes); - - if (Array.isArray(key) === true) - key = this.byteArrayToStr(key); - - const crypto = Components.classes["@mozilla.org/security/hmac;1"] - .createInstance(Components.interfaces.nsICryptoHMAC); - const keyObject = Components.classes["@mozilla.org/security/keyobjectfactory;1"] - .getService(Components.interfaces.nsIKeyObjectFactory) - .keyFromString(Components.interfaces.nsIKeyObject.HMAC, key); - - crypto.init(this.getCryptoHMAC(), keyObject); - crypto.update(bytes, bytes.length); - - const rv = this.strToByteArray(crypto.finish(false)); - - if (typeof (output) !== "undefined" && output === "hex") - return this.byteArrayToHexString(rv); - - return rv; - } - - /** - * @inheritdoc - */ - H(bytes, output) { - - if (Array.isArray(bytes) === false) - bytes = this.strToByteArray(bytes); - - const crypto = Components.classes["@mozilla.org/security/hash;1"] - .createInstance(Components.interfaces.nsICryptoHash); - - crypto.init(this.getCryptoHash()); - crypto.update(bytes, bytes.length); - - const rv = this.strToByteArray(crypto.finish(false)); - - if (typeof (output) !== "undefined" && output === "hex") - return this.byteArrayToHexString(rv); - - return rv; - } - - } - - exports.SieveCrypto = SieveMozCrypto; - -})(module.exports); diff --git a/src/wx/libs/libManageSieve/SieveCrypto.mjs b/src/wx/libs/libManageSieve/SieveCrypto.mjs new file mode 100644 index 00000000..39a8aeb6 --- /dev/null +++ b/src/wx/libs/libManageSieve/SieveCrypto.mjs @@ -0,0 +1,141 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + + +import {SieveAbstractCrypto } from "./SieveAbstractCrypto.mjs"; + +// eslint-disable-next-line no-magic-numbers +const HASH_SHA1_LENGTH = 20 * 8; +// eslint-disable-next-line no-magic-numbers +const HASH_SHA256_LENGTH = 32 * 8; +// eslint-disable-next-line no-magic-numbers +const HASH_SHA512_LENGTH = 64 * 8; + +const HASH_SHA1 = "SHA-1"; +const HASH_SHA256 = "SHA-256"; +const HASH_SHA512 = "SHA-512"; + + +/** + * Implements a crypto provider which is backed by the web crypto api. + */ +class SieveWebCrypto extends SieveAbstractCrypto { + + /** + * Returns the hash length in bits. + * + * @returns {int} + * the hash length in bits + */ + getCryptoHashLength() { + if (this.getCryptoHash() === HASH_SHA1) + return HASH_SHA1_LENGTH; + + if (this.getCryptoHash() === HASH_SHA256) + return HASH_SHA256_LENGTH; + + if (this.getCryptoHash() === HASH_SHA512) + return HASH_SHA512_LENGTH; + + throw new Error(`Unknown Hash algorithm ${this.name}`); + } + + + /** + * Hi() is a PBKDF2 [RFC2898] implementation with HMAC() as the pseudo random + * function (PRF) and with dkLen == output length of HMAC() == output + * length of H(). + * + * @param {Uint8Array} key + * the key as byte array + * @param {Uint8Array} salt + * the salt as byte array + * @param {int} iterations + * iteration count a positive number (>= 1), suggested to be at least 4096 + * + * @returns {Uint8Array} + * the pseudorandom value as byte string + */ + async Hi(key, salt, iterations) { + + if (!(key instanceof Uint8Array)) + throw new Error("Key is not a Byte Array"); + + if (!(salt instanceof Uint8Array)) + throw new Error("Salt is not a byte array"); + + // Create the base key to derive from. + key = await crypto.subtle.importKey( + "raw", key, "PBKDF2", false, ["deriveBits"]); + + const algorithm = { + "name": "PBKDF2", + "hash": this.getCryptoHash(), + "salt": salt, + "iterations": iterations + }; + + return new Uint8Array( + await window.crypto.subtle.deriveBits( + algorithm, key, this.getCryptoHashLength())); + } + + /** + * @inheritdoc + */ + async HMAC(key, bytes, output) { + + if (!Array.isArray(key) && !(key instanceof Uint8Array)) + key = (new TextEncoder()).encode(key); + + if (!Array.isArray(bytes)) + bytes = (new TextEncoder()).encode(bytes); + + key = await crypto.subtle.importKey( + "raw", new Uint8Array(key), + { name: "HMAC", hash: { name: this.getCryptoHash() } }, + false, ["sign", "verify"]); + + const signature = new Uint8Array(await crypto.subtle.sign( + "HMAC", key, new Uint8Array(bytes))); + + if (typeof (output) !== "undefined" && output === "hex") { + const rv = this.byteArrayToHexString(signature); + return rv; + } + + return [...signature]; + } + + /** + * @inheritdoc + */ + async H(bytes, output) { + + if (!Array.isArray(bytes)) { + // bytes = this.strToByteArray(bytes); + bytes = (new TextEncoder()).encode(bytes); + } + + // this.name is the algorithm. + const hash = new Uint8Array(await crypto.subtle + .digest(this.getCryptoHash(), new Uint8Array(bytes))); + + if (typeof (output) !== "undefined" && output === "hex") { + const rv = this.byteArrayToHexString(hash); + return rv; + } + + return [...hash]; + } +} + +export { SieveWebCrypto as SieveCrypto }; diff --git a/src/wx/libs/libManageSieve/SieveLogger.js b/src/wx/libs/libManageSieve/SieveLogger.js deleted file mode 100644 index bbdb30da..00000000 --- a/src/wx/libs/libManageSieve/SieveLogger.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - /* global Components */ - - const { SieveAbstractLogger } = require("./SieveAbstractLogger.js"); - - /** - * A mozilla specific logger - */ - class SieveMozLogger extends SieveAbstractLogger { - - /** - * @inheritdoc - **/ - log(message, level) { - - if (!this.isLoggable(level)) - return this; - - Components.classes["@mozilla.org/consoleservice;1"] - .getService(Components.interfaces.nsIConsoleService) - .logStringMessage(`[${this.getTimestamp()} ${this.prefix()}] ${message}`); - - return this; - } - } - - exports.SieveLogger = SieveMozLogger; - -})(module.exports); - diff --git a/src/wx/libs/libManageSieve/SieveRequestBuilder.js b/src/wx/libs/libManageSieve/SieveRequestBuilder.js deleted file mode 100644 index 1b35cbf8..00000000 --- a/src/wx/libs/libManageSieve/SieveRequestBuilder.js +++ /dev/null @@ -1,70 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - const { SieveAbstractRequestBuilder } = require("./SieveAbstractRequestBuilder.js"); - - /** - * @inheritdoc - */ - class SieveMozRequestBuilder extends SieveAbstractRequestBuilder { - - /** - * Manage Sieve uses for literals UTF-8 as encoding, network sockets are usually - * binary, and javascript is something in between. This means we have to convert - * UTF-8 into a binary by our own... - * - * @param {string} str The binary string which should be converted - * @returns {string} The converted string in UTF8 - * - * @author Thomas Schmid <schmid-thomas@gmx.net> - */ - jsStringToByteArray(str) { - // with chrome we have to use the TextEncoder. - const data = new Uint8Array(new TextEncoder("UTF-8").encode(str)); - return Array.prototype.slice.call(data); - } - - /** - * @inheritdoc - */ - calculateByteLength(data) { - return this.jsStringToByteArray(data).length; - } - - /** - * @inheritdoc - */ - convertToBase64(decoded) { - - // btoa is a bit strange it requires a javascript (unicode) string - // which contains only latin1 code point. - - if (Array.isArray(decoded)) - decoded = String.fromCharCode(...new Uint8Array(decoded)); - - return btoa(decoded); - } - - /** - * @inheritdoc - **/ - convertFromBase64(encoded) { - return atob(encoded); - } - } - - exports.SieveRequestBuilder = SieveMozRequestBuilder; - -})(module.exports); diff --git a/src/wx/libs/libManageSieve/SieveResponseParser.js b/src/wx/libs/libManageSieve/SieveResponseParser.js deleted file mode 100644 index 26a49c4e..00000000 --- a/src/wx/libs/libManageSieve/SieveResponseParser.js +++ /dev/null @@ -1,49 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - const { SieveAbstractResponseParser } = require("./SieveAbstractResponseParser.js"); - - /** - * Implements a mozilla specific response parser - **/ - class SieveMozResponseParser extends SieveAbstractResponseParser { - - /** - * @inheritdoc - **/ - convertToString(byteArray) { - // The new code should run with Google and Mozilla - byteArray = new Uint8Array(byteArray); - return (new TextDecoder("UTF-8")).decode(byteArray); - } - - /** - * @inheritdoc - **/ - convertToBase64(decoded) { - return btoa(decoded); - } - - /** - * @inheritdoc - **/ - convertFromBase64(encoded) { - return atob(encoded); - } - } - - exports.SieveResponseParser = SieveMozResponseParser; - -})(module.exports); diff --git a/src/wx/libs/libManageSieve/SieveSession.mjs b/src/wx/libs/libManageSieve/SieveSession.mjs new file mode 100644 index 00000000..57ddbd11 --- /dev/null +++ b/src/wx/libs/libManageSieve/SieveSession.mjs @@ -0,0 +1,80 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +import { SieveAbstractSession } from "./SieveAbstractSession.mjs"; + +/** + * A mozilla specific session implementation. + */ +class SieveMozSession extends SieveAbstractSession { + + /** + * @inheritdoc + */ + async onError(error) { + + if (this.listeners && this.listeners.onError) { + await this.listeners.onError(error); + return; + } + + super.onError(error); + } + + /** + * @inheritdoc + */ + async onDisconnected() { + + if (this.listeners && this.listeners.onDisconnected) { + await this.listeners.onDisconnected(); + return; + } + + super.onDisconnected(); + } + + /** + * @inheritdoc + */ + async connect(host, port) { + + // eslint-disable-next-line no-async-promise-executor + await new Promise(async (resolve, reject) => { + + try { + this.on("error", (error) => { + this.getLogger().logState("SieveSession:connect:onError()"); + reject(error); + }); + + this.on("disconnected", () => { + this.getLogger().logState("SieveSession:connect:onDisconnected()"); + // reject(new Error(`Server disconnected`)); + }); + + await super.connect(host, port); + resolve(); + } + catch (ex) { + reject(ex); + } finally { + // Restore the original listener. + this.on("error"); + this.on("disconnected"); + } + }); + + return this; + } +} + +export { SieveMozSession as SieveSession }; diff --git a/src/wx/libs/libManageSieve/SieveTimer.mjs b/src/wx/libs/libManageSieve/SieveTimer.mjs new file mode 100644 index 00000000..de9e9e02 --- /dev/null +++ b/src/wx/libs/libManageSieve/SieveTimer.mjs @@ -0,0 +1,45 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + + +import { SieveAbstractTimer } from "./SieveAbstractTimer.mjs"; + +/** + * Uses JavaScript's setTimeout() method to implement a timer. + */ +class SieveWebTimer extends SieveAbstractTimer { + + + /** + * @inheritdoc + */ + cancel() { + if (!this.timer) + return; + + window.clearTimeout(this.timer); + this.timer = null; + } + + /** + * @inheritdoc + */ + start(callback, ms) { + this.cancel(); + + if (ms === 0) + return; + + this.timer = window.setTimeout(callback, ms); + } +} + +export { SieveWebTimer as SieveTimer }; diff --git a/src/wx/libs/managesieve.ui/accounts.html b/src/wx/libs/managesieve.ui/accounts.html index 6bf8486a..1932b990 100644 --- a/src/wx/libs/managesieve.ui/accounts.html +++ b/src/wx/libs/managesieve.ui/accounts.html @@ -23,27 +23,7 @@ <!-- Placed at the end of the document so the pages load faster --> <script src="./../bootstrap/js/bootstrap.bundle.min.js"></script> - <script src="./utils/SieveFakeRequire.js"></script> - - <script src="./utils/SieveLogger.js"></script> - <script src="./utils/SieveUniqueId.js"></script> - <script src="./utils/SieveAbstractIpcClient.js"></script> - <script src="./utils/SieveIpcClient.js"></script> - - <script src="./utils/SieveI18n.js"></script> - <script src="./utils/SieveTemplate.js"></script> - - <script src="./settings/ui/SieveDebugSettingsUI.js"></script> - - <script src="./accounts/SieveCapabilities.js"></script> - <script src="./accounts/SieveScriptUI.js"></script> - <script src="./accounts/SieveAccountUI.js"></script> - <script src="./accounts/SieveAbstractAccounts.js"></script> - <script src="./accounts/SieveAccounts.js"></script> - - <script src="./dialogs/SieveDialogUI.js"></script> - - <script src="accounts.js"></script> + <script type="module" src="accounts.mjs"></script> </body> diff --git a/src/wx/libs/managesieve.ui/accounts.js b/src/wx/libs/managesieve.ui/accounts.js deleted file mode 100644 index eeec5ef9..00000000 --- a/src/wx/libs/managesieve.ui/accounts.js +++ /dev/null @@ -1,123 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function () { - - "use strict"; - - /* global SieveAccounts */ - /* global SieveIpcClient */ - /* global SieveRenameScriptDialog */ - /* global SieveCreateScriptDialog */ - /* global SieveDeleteScriptDialog */ - /* global SieveScriptBusyDialog */ - /* global SieveFingerprintDialog */ - /* global SieveLogger */ - /* global SieveI18n */ - - /** - * Shows a prompt which asks the user for the new script name. - * - * @returns {string} - * the script name or an empty string in case the dialog was canceled. - */ - async function onCreateScript() { - return await (new SieveCreateScriptDialog()).show(); - } - - /** - * Shows a prompt which asks the user if the script should be deleted. - * - * @param {string} name - * the script name which should be deleted - * - * @returns {boolean} - * true in case the script shall be deleted otherwise false. - */ - async function onDeleteScript(name) { - return await (new SieveDeleteScriptDialog(name)).show(); - } - - /** - * Shows a prompt which asks the user if the script should be renamed. - * - * @param {string} name - * the name which should be renamed - * - * @returns {string} - * the script name in case the dialog. In case the dialog was - * canceled the original name otherwise the new name. - */ - async function onRenameScript(name) { - return await (new SieveRenameScriptDialog(name)).show(); - } - - /** - * Informs the user that the action can't be performed because - * the script is currently in use. - * - * @param {string} name - * the name of the script which was busy - */ - async function onBusy(name) { - await (new SieveScriptBusyDialog(name)).show(); - } - - /** - * Informs the user about a failed certificate validation. - * - * @param {object} secInfo - * the security information with more details about the validation error. - * - * @returns {boolean} - * true in case the certificate should be overwritten otherwise false. - */ - async function onCertError(secInfo) { - return await (new SieveFingerprintDialog(secInfo)).show(); - } - - /** - * The main entry point for the account view - */ - async function main() { - - // TODO move to editor - /* window.onbeforeunload = (e) => { - // if changed... - e.preventDefault(); - };*/ - - SieveLogger.getInstance().level( - await SieveIpcClient.sendMessage("core", "settings-get-loglevel")); - - await (SieveI18n.getInstance()).load(); - - const accounts = new SieveAccounts(); - accounts.render(); - - SieveIpcClient.setRequestHandler("accounts", "script-show-create", - async () => { return await onCreateScript(); }); - SieveIpcClient.setRequestHandler("accounts", "script-show-delete", - async (msg) => { return await onDeleteScript(msg.payload); }); - SieveIpcClient.setRequestHandler("accounts", "script-show-rename", - async (msg) => { return await onRenameScript(msg.payload); }); - SieveIpcClient.setRequestHandler("accounts", "script-show-busy", - async (msg) => { await onBusy(msg.payload); }); - SieveIpcClient.setRequestHandler("accounts", "account-show-certerror", - async (msg) => { return await onCertError(msg.payload); }); - } - - if (document.readyState !== 'loading') - main(); - else - document.addEventListener('DOMContentLoaded', () => { main(); }, {once: true}); - -})(); diff --git a/src/wx/libs/managesieve.ui/accounts.mjs b/src/wx/libs/managesieve.ui/accounts.mjs new file mode 100644 index 00000000..9eea29c3 --- /dev/null +++ b/src/wx/libs/managesieve.ui/accounts.mjs @@ -0,0 +1,137 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +import { SieveLogger } from "./utils/SieveLogger.mjs"; +import { SieveIpcClient } from "./utils/SieveIpcClient.mjs"; +import { SieveI18n } from "./utils/SieveI18n.mjs"; + +import { SieveAbstractAccounts as SieveAccounts } from "./accounts/SieveAbstractAccounts.mjs"; +import { + SieveCreateScriptDialog, + SieveDeleteScriptDialog, + SieveRenameScriptDialog, + SieveFingerprintDialog, + SieveScriptBusyDialog, + SieveErrorDialog +} from "./dialogs/SieveDialogUI.mjs"; + +/** + * Shows a prompt which asks the user for the new script name. + * + * @returns {string} + * the script name or an empty string in case the dialog was canceled. + */ +async function onCreateScript() { + return await (new SieveCreateScriptDialog()).show(); +} + +/** + * Shows a prompt which asks the user if the script should be deleted. + * + * @param {string} name + * the script name which should be deleted + * + * @returns {boolean} + * true in case the script shall be deleted otherwise false. + */ +async function onDeleteScript(name) { + return await (new SieveDeleteScriptDialog(name)).show(); +} + +/** + * Shows a prompt which asks the user if the script should be renamed. + * + * @param {string} name + * the name which should be renamed + * + * @returns {string} + * the script name in case the dialog. In case the dialog was + * canceled the original name otherwise the new name. + */ +async function onRenameScript(name) { + return await (new SieveRenameScriptDialog(name)).show(); +} + +/** + * Informs the user that the action can't be performed because + * the script is currently in use. + * + * @param {string} name + * the name of the script which was busy + */ +async function onBusy(name) { + await (new SieveScriptBusyDialog(name)).show(); +} + +/** + * Informs the user about a failed certificate validation. + * + * @param {object} secInfo + * the security information with more details about the validation error. + * + * @returns {boolean} + * true in case the certificate should be overwritten otherwise false. + */ +async function onCertError(secInfo) { + return await (new SieveFingerprintDialog(secInfo)).show(); +} + +/** + * Informs the user about a connection error. + * + * @param {string} message + * the detailed connection error. + */ +async function onError(message) { + await (new SieveErrorDialog(message)).show(); +} + +/** + * The main entry point for the account view + */ +async function main() { + + // TODO move to editor + /* window.onbeforeunload = (e) => { + // if changed... + e.preventDefault(); + };*/ + try { + + SieveLogger.getInstance().level( + await SieveIpcClient.sendMessage("core", "settings-get-loglevel")); + + await (SieveI18n.getInstance()).load(); + + const accounts = new SieveAccounts(); + accounts.render(); + + SieveIpcClient.setRequestHandler("accounts", "script-show-create", + async () => { return await onCreateScript(); }); + SieveIpcClient.setRequestHandler("accounts", "script-show-delete", + async (msg) => { return await onDeleteScript(msg.payload); }); + SieveIpcClient.setRequestHandler("accounts", "script-show-rename", + async (msg) => { return await onRenameScript(msg.payload); }); + SieveIpcClient.setRequestHandler("accounts", "script-show-busy", + async (msg) => { await onBusy(msg.payload); }); + SieveIpcClient.setRequestHandler("accounts", "account-show-certerror", + async (msg) => { return await onCertError(msg.payload); }); + SieveIpcClient.setRequestHandler("accounts", "account-show-error", + async (msg) => { return await onError(msg.payload); }); + } catch (ex) { + console.error(ex); + } +} + +if (document.readyState !== 'loading') + main(); +else + document.addEventListener('DOMContentLoaded', () => { main(); }, { once: true }); diff --git a/src/wx/libs/managesieve.ui/accounts/SieveAccountUI.mjs b/src/wx/libs/managesieve.ui/accounts/SieveAccountUI.mjs new file mode 100644 index 00000000..e9fc4c28 --- /dev/null +++ b/src/wx/libs/managesieve.ui/accounts/SieveAccountUI.mjs @@ -0,0 +1,21 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +import { SieveAbstractAccountUI } from "./SieveAbstractAccountUI.mjs"; + + +/** + * A UI renderer for a sieve account + */ +class SieveMozAccountUI extends SieveAbstractAccountUI { +} + +export { SieveMozAccountUI as SieveAccountUI }; diff --git a/src/wx/libs/managesieve.ui/accounts/SieveAccounts.js b/src/wx/libs/managesieve.ui/accounts/SieveAccounts.js deleted file mode 100644 index 69726d66..00000000 --- a/src/wx/libs/managesieve.ui/accounts/SieveAccounts.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - /* global SieveAbstractAccounts */ - - /** - * @inheritdoc - */ - class SieveWxAccounts extends SieveAbstractAccounts { - - } - - if (typeof (module) !== "undefined" && module && module.exports) - module.exports.SieveAccounts = SieveWxAccounts; - else - exports.SieveAccounts = SieveWxAccounts; - -})(this); diff --git a/src/wx/libs/managesieve.ui/accounts/SieveAccounts.mjs b/src/wx/libs/managesieve.ui/accounts/SieveAccounts.mjs new file mode 100644 index 00000000..d0050e4b --- /dev/null +++ b/src/wx/libs/managesieve.ui/accounts/SieveAccounts.mjs @@ -0,0 +1,22 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + + +import { SieveAbstractAccounts } from "./SieveAbstractAccounts.mjs"; + +/** + * @inheritdoc + */ +class SieveWxAccounts extends SieveAbstractAccounts { + +} + +export { SieveWxAccounts as SieveAccounts }; diff --git a/src/wx/libs/managesieve.ui/accounts/account.settings.tpl b/src/wx/libs/managesieve.ui/accounts/account.settings.html index db522a96..9f83be38 100644 --- a/src/wx/libs/managesieve.ui/accounts/account.settings.tpl +++ b/src/wx/libs/managesieve.ui/accounts/account.settings.html @@ -4,7 +4,7 @@ <div style="min-width:8em" data-i18n="account.details.server"></div> <span class="sieve-settings-hostname"></span>: <span class="sieve-settings-port"></span> - <span class="sieve-settings-secure ml-1" data-i18n="account.details.secure"></span> + <span class="sieve-settings-secure ms-1" data-i18n="account.details.secure"></span> </div> <!--<div class="row sieve-settings-fingerprint-item"> <span class="col-3">Fingerprint:</span> @@ -19,8 +19,8 @@ <span class="sieve-settings-mechanism">SCRAM-MD5</span> </div>--> <div class="mt-3"> - <!-- <button type="button" class="sieve-account-edit-server btn btn-sm btn-outline-secondary mr-1">Edit Server</button> - <button type="button" class="sieve-account-edit-credentials btn btn-sm btn-outline-secondary mr-1">Edit Credentials</button>--> + <!-- <button type="button" class="sieve-account-edit-server btn btn-sm btn-outline-secondary me-1">Edit Server</button> + <button type="button" class="sieve-account-edit-credentials btn btn-sm btn-outline-secondary me-1">Edit Credentials</button>--> <button type="button" data-i18n="account.details.debugging.edit" class="sieve-account-edit-debug btn btn-sm btn-outline-secondary"></button> diff --git a/src/wx/libs/managesieve.ui/settings/logic/SieveAccounts.js b/src/wx/libs/managesieve.ui/settings/logic/SieveAccounts.js deleted file mode 100644 index 1fdc49ab..00000000 --- a/src/wx/libs/managesieve.ui/settings/logic/SieveAccounts.js +++ /dev/null @@ -1,61 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - /* global browser */ - const { SieveAbstractAccount } = require("./SieveAbstractAccount.js"); - const { SieveAbstractAccounts } = require("./SieveAbstractAccounts.js"); - - - /** - * Manages the configuration for sieve accounts. - * It queries thunderbird's account and extracts all needed information. - * - * Global settings are stored in the addons persistence. - */ - class SieveAccounts extends SieveAbstractAccounts{ - - /** - * @inheritdoc - */ - async load() { - - const items = await (browser.accounts.list()); - - const accounts = {}; - - if (!items) - return this; - - for (const item of items) { - - if (item.type !== "imap" && item.type !== "pop3") - continue; - - accounts[item.id] = new SieveAbstractAccount(item.id); - } - - this.accounts = accounts; - return this; - } - - } - - // Require modules need to use export.module - if (typeof (module) !== "undefined" && module && module.exports) - module.exports.SieveAccounts = SieveAccounts; - else - exports.SieveAccounts = SieveAccounts; - -})(this); diff --git a/src/wx/libs/managesieve.ui/settings/logic/SieveAccounts.mjs b/src/wx/libs/managesieve.ui/settings/logic/SieveAccounts.mjs new file mode 100644 index 00000000..78e5562b --- /dev/null +++ b/src/wx/libs/managesieve.ui/settings/logic/SieveAccounts.mjs @@ -0,0 +1,50 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +/* global browser */ +import { SieveAbstractAccount } from "./SieveAbstractAccount.mjs"; +import { SieveAbstractAccounts } from "./SieveAbstractAccounts.mjs"; + +/** + * Manages the configuration for sieve accounts. + * It queries thunderbird's account and extracts all needed information. + * + * Global settings are stored in the addons persistence. + */ +class SieveAccounts extends SieveAbstractAccounts { + + /** + * @inheritdoc + */ + async load() { + + const items = await (browser.accounts.list()); + + const accounts = {}; + + if (!items) + return this; + + for (const item of items) { + + if (item.type !== "imap" && item.type !== "pop3") + continue; + + accounts[item.id] = new SieveAbstractAccount(item.id); + } + + this.accounts = accounts; + return this; + } + +} + +export { SieveAccounts }; diff --git a/src/wx/libs/managesieve.ui/settings/logic/SieveAuthentication.js b/src/wx/libs/managesieve.ui/settings/logic/SieveAuthentication.js deleted file mode 100644 index 0f717941..00000000 --- a/src/wx/libs/managesieve.ui/settings/logic/SieveAuthentication.js +++ /dev/null @@ -1,96 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - const AUTH_TYPE_IMAP = 1; - const DEFAULT_AUTH_TYPE = AUTH_TYPE_IMAP; - - const CONFIG_AUTHENTICATION_TYPE = "activeLogin"; - - const { SieveAbstractAuthentication } = require("libs/managesieve.ui/settings/SieveAbstractAuthentication.js"); - const { SieveAbstractMechanism } = require("libs/managesieve.ui/settings/SieveAbstractMechanism.js"); - - /* global browser */ - - /** - * Uses the IMAP accounts credentials. - */ - class SieveImapAuthentication extends SieveAbstractAuthentication { - - /** - * @inheritdoc - */ - async getPassword() { - return await browser.sieve.accounts.getPassword(this.account.getId()); - } - - /** - * @inheritdoc - */ - async getUsername() { - return await browser.sieve.accounts.getUsername(this.account.getId()); - } - } - - /** - * Manages the authorization settings. - */ - class SieveAuthentication extends SieveAbstractMechanism { - - /** - * @inheritdoc - **/ - getDefault() { - return DEFAULT_AUTH_TYPE; - } - - /** - * @inheritdoc - **/ - getKey() { - return CONFIG_AUTHENTICATION_TYPE; - } - - /** - * @inheritdoc - **/ - hasMechanism(type) { - switch (type) { - case AUTH_TYPE_IMAP: - return true; - - default: - return false; - } - } - - /** - * @inheritdoc - **/ - getMechanismById(type) { - switch (type) { - case AUTH_TYPE_IMAP: - // fall through we just implement prompt authentication - default: - return new SieveImapAuthentication(AUTH_TYPE_IMAP, this.account); - } - } - } - - if (typeof (module) !== "undefined" && module && module.exports) - module.exports.SieveAuthentication = SieveAuthentication; - else - exports.SieveAuthentication = SieveAuthentication; - -})(this); diff --git a/src/wx/libs/managesieve.ui/settings/logic/SieveAuthentication.mjs b/src/wx/libs/managesieve.ui/settings/logic/SieveAuthentication.mjs new file mode 100644 index 00000000..df3067c8 --- /dev/null +++ b/src/wx/libs/managesieve.ui/settings/logic/SieveAuthentication.mjs @@ -0,0 +1,37 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +import { SieveAbstractAuthentication } from "./SieveAbstractAuthentication.mjs"; + +/* global browser */ + +/** + * Uses the IMAP accounts credentials. + */ +class SieveMozAuthentication extends SieveAbstractAuthentication { + + /** + * @inheritdoc + */ + async getPassword() { + return await browser.sieve.accounts.getPassword(this.account.getId()); + } + + /** + * @inheritdoc + */ + async getUsername() { + return await browser.sieve.accounts.getUsername(this.account.getId()); + } +} + +export { SieveMozAuthentication as SieveAuthentication }; + diff --git a/src/wx/libs/managesieve.ui/settings/logic/SieveAuthorization.js b/src/wx/libs/managesieve.ui/settings/logic/SieveAuthorization.js deleted file mode 100644 index 889b62bf..00000000 --- a/src/wx/libs/managesieve.ui/settings/logic/SieveAuthorization.js +++ /dev/null @@ -1,75 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - const AUTHORIZATION_TYPE_USERNAME = 1; - const CONFIG_AUTHORIZATION_TYPE = "authorization.type"; - - const { SieveAbstractMechanism } = require("libs/managesieve.ui/settings/SieveAbstractMechanism.js"); - const { SieveDefaultAuthorization } = require("libs/managesieve.ui/settings/SieveAbstractAuthorization.js"); - - /** - * Manages the authorization settings. - */ - class SieveAuthorization extends SieveAbstractMechanism { - - /** - * @inheritdoc - **/ - getDefault() { - return AUTHORIZATION_TYPE_USERNAME; - } - - /** - * @inheritdoc - **/ - getKey() { - return CONFIG_AUTHORIZATION_TYPE; - } - - /** - * @inheritdoc - **/ - hasMechanism(type) { - switch (type) { - case AUTHORIZATION_TYPE_USERNAME: - return true; - - default: - return false; - } - } - - /** - * @inheritdoc - **/ - getMechanismById(type) { - switch (type) { - case AUTHORIZATION_TYPE_USERNAME: - return new SieveDefaultAuthorization(AUTHORIZATION_TYPE_USERNAME, this.account); - - default: - throw new Error("Unknown authorization mechanism"); - } - } - } - - - // Require modules need to use export.module - if (typeof (module) !== "undefined" && module && module.exports) - module.exports.SieveAuthorization = SieveAuthorization; - else - exports.SieveAuthorization = SieveAuthorization; - -})(this); diff --git a/src/wx/libs/managesieve.ui/settings/logic/SieveAuthorization.mjs b/src/wx/libs/managesieve.ui/settings/logic/SieveAuthorization.mjs new file mode 100644 index 00000000..7d74f0a1 --- /dev/null +++ b/src/wx/libs/managesieve.ui/settings/logic/SieveAuthorization.mjs @@ -0,0 +1,64 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +const AUTHORIZATION_TYPE_USERNAME = 1; +const CONFIG_AUTHORIZATION_TYPE = "authorization.type"; + +import { SieveAbstractMechanism } from "./SieveAbstractMechanism.mjs"; +import { SieveDefaultAuthorization } from "./SieveAbstractAuthorization.mjs"; + +/** + * Manages the authorization settings. + */ +class SieveAuthorization extends SieveAbstractMechanism { + + /** + * @inheritdoc + **/ + getDefault() { + return AUTHORIZATION_TYPE_USERNAME; + } + + /** + * @inheritdoc + **/ + getKey() { + return CONFIG_AUTHORIZATION_TYPE; + } + + /** + * @inheritdoc + **/ + hasMechanism(type) { + switch (type) { + case AUTHORIZATION_TYPE_USERNAME: + return true; + + default: + return false; + } + } + + /** + * @inheritdoc + **/ + getMechanismById(type) { + switch (type) { + case AUTHORIZATION_TYPE_USERNAME: + return new SieveDefaultAuthorization(AUTHORIZATION_TYPE_USERNAME, this.account); + + default: + throw new Error("Unknown authorization mechanism"); + } + } +} + +export { SieveAuthorization }; diff --git a/src/wx/libs/managesieve.ui/settings/logic/SieveHost.js b/src/wx/libs/managesieve.ui/settings/logic/SieveHost.js deleted file mode 100644 index 8eae489e..00000000 --- a/src/wx/libs/managesieve.ui/settings/logic/SieveHost.js +++ /dev/null @@ -1,100 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - /* global browser */ - - const HOST_TYPE_IMAP = 0; - const CONFIG_HOST_TYPE = "activeHost"; - - const { SieveAbstractMechanism } = require("libs/managesieve.ui/settings/SieveAbstractMechanism.js"); - const { SieveAbstractHost } = require("libs/managesieve.ui/settings/SieveAbstractHost.js"); - - /** - * This class loads the hostname from an IMAP account. The hostname is not - * cached it. This ensures that always the most recent settings are used. - */ - class SieveImapHost extends SieveAbstractHost { - - /** - * @inheritdoc - */ - async getDisplayName() { - return await browser.sieve.accounts.getPrettyName(this.account.getId()); - } - - /** - * @inheritdoc - */ - async getHostname() { - return await browser.sieve.accounts.getHostname(this.account.getId()); - } - } - - /** - * A transparent wrapper needed to deal with the different - * host mechanism which are provided by electron and thunderbird. - **/ - class SieveHost extends SieveAbstractMechanism { - - /** - * @inheritdoc - **/ - getKey() { - return CONFIG_HOST_TYPE; - } - - /** - * @inheritdoc - **/ - getDefault() { - return HOST_TYPE_IMAP; - } - - /** - * @inheritdoc - */ - hasMechanism(type) { - switch (type) { - case HOST_TYPE_IMAP: - return true; - - default: - return false; - } - } - - /** - * @inheritdoc - */ - getMechanismById(type) { - - switch (type) { - - case HOST_TYPE_IMAP: - // fall through - default: - return new SieveImapHost(HOST_TYPE_IMAP, this.account); - } - } - - } - - // Require modules need to use export.module - if (typeof (module) !== "undefined" && module && module.exports) - module.exports.SieveHost = SieveHost; - else - exports.SieveHost = SieveHost; - -})(this); diff --git a/src/wx/libs/managesieve.ui/settings/logic/SieveHost.mjs b/src/wx/libs/managesieve.ui/settings/logic/SieveHost.mjs new file mode 100644 index 00000000..102c2ced --- /dev/null +++ b/src/wx/libs/managesieve.ui/settings/logic/SieveHost.mjs @@ -0,0 +1,50 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +/* global browser */ + +import { SieveCustomHost } from "./SieveAbstractHost.mjs"; + +const CONFIG_KEEP_ALIVE_INTERVAL = "keepalive"; +// eslint-disable-next-line no-magic-numbers +const ONE_MINUTE = 60 * 1000; +// eslint-disable-next-line no-magic-numbers +const FIVE_MINUTES = 5 * ONE_MINUTE; + +/** + * This class loads the hostname from an IMAP account. The hostname is not + * cached it. This ensures that always the most recent settings are used. + */ +class SieveMozHost extends SieveCustomHost { + + /** + * @inheritdoc + */ + async getDisplayName() { + return await browser.sieve.accounts.getPrettyName(this.account.getId()); + } + + /** + * @inheritdoc + */ + async getHostname() { + return await browser.sieve.accounts.getHostname(this.account.getId()); + } + + /** + * @inheritdoc + */ + async getKeepAlive() { + return await this.account.getConfig().getInteger(CONFIG_KEEP_ALIVE_INTERVAL, FIVE_MINUTES); + } +} + +export { SieveMozHost as SieveHost }; diff --git a/src/wx/libs/managesieve.ui/settings/logic/SievePrefManager.js b/src/wx/libs/managesieve.ui/settings/logic/SievePrefManager.js deleted file mode 100644 index 784c6777..00000000 --- a/src/wx/libs/managesieve.ui/settings/logic/SievePrefManager.js +++ /dev/null @@ -1,58 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - /* global browser */ - - const { SieveAbstractPrefManager } = require("libs/managesieve.ui/settings/SieveAbstractPrefManager.js"); - - /** - * Manages preferences. - * It uses the WebExtension's local storage interface - */ - class SieveMozPrefManager extends SieveAbstractPrefManager { - - /** - * @inheritdoc - */ - async getValue(key) { - key = `${this.getNamespace()}.${key}`; - - const pair = await browser.storage.local.get(key); - - if (pair[key] === undefined) - return undefined; - - return pair[key]; - } - - /** - * @inheritdoc - */ - async setValue(key, value) { - - const item = {}; - item[`${this.getNamespace()}.${key}`] = value; - - await browser.storage.local.set(item); - return this; - } - } - - if (typeof (module) !== "undefined" && module && module.exports) - module.exports.SievePrefManager = SieveMozPrefManager; - else - exports.SievePrefManager = SieveMozPrefManager; - -})(this); diff --git a/src/wx/libs/managesieve.ui/settings/logic/SievePrefManager.mjs b/src/wx/libs/managesieve.ui/settings/logic/SievePrefManager.mjs new file mode 100644 index 00000000..60d78340 --- /dev/null +++ b/src/wx/libs/managesieve.ui/settings/logic/SievePrefManager.mjs @@ -0,0 +1,49 @@ +/* + * The content of this file is licensed. You may obtain a copy of + * the license at https://github.com/thsmi/sieve/ or request it via + * email from the author. + * + * Do not remove or change this comment. + * + * The initial author of the code is: + * Thomas Schmid <schmid-thomas@gmx.net> + */ + +/* global browser */ +import { SieveAbstractPrefManager } from "./SieveAbstractPrefManager.mjs"; + +/** + * Manages preferences. + * It uses the WebExtension's local storage interface + */ +class SieveMozPrefManager extends SieveAbstractPrefManager { + + /** + * @inheritdoc + */ + async getValue(key) { + key = `${this.getNamespace()}.${key}`; + + const pair = await browser.storage.local.get(key); + + if (pair[key] === undefined) + return undefined; + + return pair[key]; + } + + /** + * @inheritdoc + */ + async setValue(key, value) { + + const item = {}; + item[`${this.getNamespace()}.${key}`] = value; + + await browser.storage.local.set(item); + return this; + } +} + + +export { SieveMozPrefManager as SievePrefManager }; diff --git a/src/wx/libs/managesieve.ui/settings/logic/SieveSecurity.js b/src/wx/libs/managesieve.ui/settings/logic/SieveSecurity.js deleted file mode 100644 index 79b5e3b8..00000000 --- a/src/wx/libs/managesieve.ui/settings/logic/SieveSecurity.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -(function (exports) { - - "use strict"; - - /** - * Defines the security related settings for an account. - * It is a minimal, mozilla specific implementation. - */ - class SieveSecurity { - - /** - * @inheritdoc - */ - async isSecure() { - return await true; - } - - /** - * @inheritdoc - */ - async getMechanism() { - return await "default"; - } - } - - - // Require modules need to use export.module - if (typeof (module) !== "undefined" && module && module.exports) - module.exports.SieveSecurity = SieveSecurity; - else - exports.SieveSecurity = SieveSecurity; - -})(this); diff --git a/src/wx/libs/libManageSieve/SieveSession.js b/src/wx/libs/managesieve.ui/settings/logic/SieveSecurity.mjs index 9a164838..95eb8ed1 100644 --- a/src/wx/libs/libManageSieve/SieveSession.js +++ b/src/wx/libs/managesieve.ui/settings/logic/SieveSecurity.mjs @@ -9,12 +9,8 @@ * Thomas Schmid <schmid-thomas@gmx.net> */ -(function (exports) { - "use strict"; +import { SieveAbstractSecurity } from "./SieveAbstractSecurity.mjs"; - const { SieveAbstractSession } = require("./SieveAbstractSession.js"); +export { SieveAbstractSecurity as SieveSecurity }; - exports.SieveSession = SieveAbstractSession; - -})(module.exports); diff --git a/src/wx/libs/managesieve.ui/utils/SieveIpcClient.js b/src/wx/libs/managesieve.ui/utils/SieveIpcClient.js index dfef1461..cff8ca78 100644 --- a/src/wx/libs/managesieve.ui/utils/SieveIpcClient.js +++ b/src/wx/libs/managesieve.ui/utils/SieveIpcClient.js @@ -9,55 +9,45 @@ * Thomas Schmid <schmid-thomas@gmx.net> */ -(function (exports) { +/* global browser */ +import { SieveLogger } from "./SieveLogger.mjs"; +import { SieveAbstractIpcClient } from "./SieveAbstractIpcClient.mjs"; - "use strict"; +/** + * An abstract implementation for a inter process/frame communication. + */ +class SieveWxIpcClient extends SieveAbstractIpcClient { - /* global browser */ - const { SieveLogger } = require("./SieveLogger.js"); - const { SieveAbstractIpcClient } = require("./SieveAbstractIpcClient.js"); + /** + * @inheritdoc + */ + static getLogger() { + return SieveLogger.getInstance(); + } /** - * An abstract implementation for a inter process/frame communication. + * @inheritdoc */ - class SieveWxIpcClient extends SieveAbstractIpcClient { + static parseMessageFromEvent(e) { + return JSON.parse(e.data); + } - /** - * @inheritdoc - */ - static getLogger() { - return SieveLogger.getInstance(); - } + /** + * @inheritdoc + */ + // eslint-disable-next-line no-unused-vars + static dispatch(message, target) { - /** - * @inheritdoc - */ - static parseMessageFromEvent(e) { - return JSON.parse(e.data); + if (typeof (message) !== 'string') { + message = JSON.stringify(message); } - /** - * @inheritdoc - */ - // eslint-disable-next-line no-unused-vars - static dispatch(message, target) { - - if (typeof(message) !== 'string') { - message = JSON.stringify(message); - } - - browser.runtime.sendMessage(message); - } + browser.runtime.sendMessage(message); } +} - browser.runtime.onMessage.addListener((request, sender) => { - SieveWxIpcClient.onMessage({data : request, source: sender}); - }); - - // Require modules need to use export.module - if (typeof(module) !== "undefined" && module && module.exports) - module.exports.SieveIpcClient = SieveWxIpcClient; - else - exports.SieveIpcClient = SieveWxIpcClient; +browser.runtime.onMessage.addListener((request, sender) => { + SieveWxIpcClient.onMessage({ data: request, source: sender }); +}); -})(this); +export { SieveWxIpcClient as SieveIpcClient }; diff --git a/src/wx/manifest.json b/src/wx/manifest.json index 026cb3f9..b5d6491b 100644 --- a/src/wx/manifest.json +++ b/src/wx/manifest.json @@ -37,17 +37,17 @@ ] } }, - "SieveSessionApi": { - "schema": "api/sieve/SieveSessionApi.json", + "SieveSocketApi": { + "schema": "api/sieve/SieveSocketApi.json", "parent": { "scopes": [ "addon_parent" ], - "script": "api/sieve/SieveSessionApi.js", + "script": "api/sieve/SieveSocketApi.js", "paths": [ [ "sieve", - "session" + "socket" ] ] } diff --git a/src/wx/modules/SieveRequire.jsm b/src/wx/modules/SieveRequire.jsm deleted file mode 100644 index 17a96487..00000000 --- a/src/wx/modules/SieveRequire.jsm +++ /dev/null @@ -1,276 +0,0 @@ -/* - * The content of this file is licensed. You may obtain a copy of - * the license at https://github.com/thsmi/sieve/ or request it via - * email from the author. - * - * Do not remove or change this comment. - * - * The initial author of the code is: - * Thomas Schmid <schmid-thomas@gmx.net> - */ - -/** - * Implements a minimal CommonJS compatible implementation. - * - * It emulates the node.js CommonJS implementation as described here - * http://fredkschott.com/post/2014/06/require-and-the-module-system/ - * - * The sandbox loading code is based on the base-loader from mozilla's devtools. - * See mozilla-central/source/devtools/shared/base-loader.js and - * mozilla-central/source/devtools/shared/loader-plugin-raw.jsm for more details. - */ -(function (exports) { - - "use strict"; - - const JS_EXTENSION = ".js"; - - /* global Components */ - /* global ChromeUtils */ - - const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); - - /** - * Manages a simple CommonJS Module. - * It implements a sandbox in which the modules code lives. - */ - class Module { - - /** - * Creates a new CommonJS Module. - * - * @param {Modules} modules - * a reference to the parent module. - * @param {string} uri - * the uri to the modules source code. - */ - constructor(modules, uri) { - - this.modules = modules; - this.uri = uri; - - const systemPrincipal = Components - .classes["@mozilla.org/systemprincipal;1"] - .createInstance(Components.interfaces.nsIPrincipal); - - // create a new scope. - this.sandbox = new Components.utils.Sandbox(systemPrincipal, { - wantGlobalProperties: ["XMLHttpRequest", "TextEncoder", "TextDecoder", "atob", "btoa"], - wantXrays: false, - freshCompartment: false - // invisibleToDebugger: true - }); - - // then push a reference to our require function to it - this.sandbox.require = (url) => { return this.modules.require(url); }; - this.sandbox.Error = Error; - // and create a dummy modules.exports - this.sandbox.module = { exports: {} }; - // and a exports. - this.sandbox.export = {}; - } - - /** - * Invalidates the module and releases the sandbox. - * This should immediately remove all references to any sandboxed element - * including listeners. - * So be care full when calling this. - */ - invalidate() { - if (this.sandbox === null) - return; - - this.modules.log(`Nuking sandbox for ${this.uri} ...`); - - delete this.sandbox.require; - delete this.sandbox.Error; - delete this.sandbox.module; - delete this.sandbox.export; - - try { - Components.utils.nukeSandbox(this.sandbox); - } catch (ex) { - // If nuke failed we just ignore it as we can't do anything about it. - } - - this.sandbox = null; - this.modules = null; - } - - /** - * Returns the module's exports - * In case the module is loaded and empty object is returned. - * - * @returns {object} - * the modules exports - */ - getExports() { - return this.sandbox.module.exports; - } - - - /** - * Reads from the given resource and returns the result. - * - * @param {string} uri - * the uri to be read - * @returns {string} - * the uri's content as string. - */ - readURI(uri) { - this.modules.log(`Reading url ${uri}`); - - const stream = NetUtil.newChannel({ - uri: NetUtil.newURI(uri, "UTF-8"), - loadUsingSystemPrincipal: true - }).open(); - - const count = stream.available(); - const data = NetUtil.readInputStreamToString(stream, count, { - charset: "UTF-8" - }); - stream.close(); - - return data; - } - - /** - * Loads a commonjs module into the sandbox. - */ - load() { - - if (this.sandbox === null) - throw new Error("Module was invalidated and cannot be reused."); - - this.modules.log(`Loading CommonJS Module from ${this.uri}`); - - const script = this.readURI(this.uri); - - Components.utils.evalInSandbox(script, this.sandbox); - - if (!this.sandbox.module || !this.sandbox.module.exports) - throw new Error(`Failed to load CommonJS Module ${this.uri}`); - - if (!Object.getOwnPropertyNames(this.sandbox.module.exports).length) - throw new Error(`Module does not export anything ${this.uri}`); - } - } - - /** - * Manages the CommonJS modules as well as their cache. - */ - class Modules { - - /** - * Creates a new instance. - * - * @param {string} base - * an optional prefix. - * - * @param {Function} [logger] - * a function which wraps a logger call. - * in case omitted the logger will be disabled - * - */ - constructor(base, logger) { - - if (base.endsWith("/")) - base = base.slice(0, -1); - - this.base = base; - - this.cache = new Map(); - - this.logger = logger; - } - - /** - * Logs a message in case a logger was set in the constructor. - * - * @param {string} msg - * the log message - */ - log(msg) { - if (typeof(this.logger) === "undefined" || this.logger === null) - return; - - this.logger(msg); - } - - /** - * Resolves and normalizes the given uri. This means it converts - * a relative uri into an absolute one. - * - * @param {string} uri - * the uri to be normalized. - * - * @returns {string} - * the normalize uri - */ - resolve(uri) { - this.log(`Resolving ${uri}`); - - if (!uri.endsWith(JS_EXTENSION)) - throw new Error(`Not a JavaScript file ${uri}`); - - if (uri.startsWith("./")) - uri = this.base + uri.substring(".".length); - - return uri; - } - - /** - * Loads a CommonJS module into this cache. - * - * @param {string} uri - * the uri of the module to be loaded. - * - * @returns {object} - * the modules exports - */ - require(uri) { - - uri = this.resolve(uri); - - this.log(`Loading CommonJS Module from ${uri}`); - - if (this.cache.has(uri)) { - this.log(`Cache hit for ${uri}`); - return this.cache.get(uri).getExports(); - } - - const module = new Module(this, uri); - this.cache.set(uri, module); - - try { - module.load(); - } catch (ex) { - this.log(ex); - - this.cache.delete(uri); - throw ex; - } - - return module.getExports(); - } - - /** - * Clears and invalidates all cached values. - */ - invalidate() { - for (const item of this.cache.values()) - item.invalidate(); - - this.cache.clear(); - } - } - - exports.Modules = Modules; - - // Expose as mozilla module... - if (!exports.EXPORTED_SYMBOLS) - exports.EXPORTED_SYMBOLS = []; - - exports.EXPORTED_SYMBOLS.push("Modules"); - -})(this); |