From a57b13d3199fe17dc50d82f0a87da03800fd7e09 Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+samtv12345@users.noreply.github.com> Date: Wed, 24 Jan 2024 09:44:17 +0100 Subject: [PATCH] Moved SecretRotator to typescript. --- src/node/models/DeriveModel.ts | 6 +++ src/node/models/LegacyParams.ts | 8 ++++ src/node/models/MapType.ts | 3 ++ src/node/models/PadType.ts | 16 +++++++ src/node/models/PromiseWithStd.ts | 4 +- .../{SecretRotator.js => SecretRotator.ts} | 47 ++++++++++++------- src/node/security/{crypto.js => crypto.ts} | 0 .../{ExportEtherpad.js => ExportEtherpad.ts} | 4 +- .../{ExportHelper.js => ExportHelper.ts} | 12 +++-- src/node/utils/{ExportTxt.js => ExportTxt.ts} | 22 +++++---- .../{checkValidRev.js => checkValidRev.ts} | 4 +- .../utils/{customError.js => customError.ts} | 4 +- .../utils/{path_exists.js => path_exists.ts} | 4 +- 13 files changed, 93 insertions(+), 41 deletions(-) create mode 100644 src/node/models/DeriveModel.ts create mode 100644 src/node/models/LegacyParams.ts create mode 100644 src/node/models/MapType.ts create mode 100644 src/node/models/PadType.ts rename src/node/security/{SecretRotator.js => SecretRotator.ts} (88%) rename src/node/security/{crypto.js => crypto.ts} (100%) rename src/node/utils/{ExportEtherpad.js => ExportEtherpad.ts} (96%) rename src/node/utils/{ExportHelper.js => ExportHelper.ts} (86%) rename src/node/utils/{ExportTxt.js => ExportTxt.ts} (92%) rename src/node/utils/{checkValidRev.js => checkValidRev.ts} (81%) rename src/node/utils/{customError.js => customError.ts} (85%) rename src/node/utils/{path_exists.js => path_exists.ts} (65%) diff --git a/src/node/models/DeriveModel.ts b/src/node/models/DeriveModel.ts new file mode 100644 index 000000000..b6297f3ce --- /dev/null +++ b/src/node/models/DeriveModel.ts @@ -0,0 +1,6 @@ +export type DeriveModel = { + digest: string, + secret: string, + salt: string, + keyLen: number +} \ No newline at end of file diff --git a/src/node/models/LegacyParams.ts b/src/node/models/LegacyParams.ts new file mode 100644 index 000000000..ea03c5618 --- /dev/null +++ b/src/node/models/LegacyParams.ts @@ -0,0 +1,8 @@ +export type LegacyParams = { + start: number, + end: number, + lifetime: number, + algId: number, + algParams: any, + interval:number|null +} \ No newline at end of file diff --git a/src/node/models/MapType.ts b/src/node/models/MapType.ts new file mode 100644 index 000000000..fc8af91d5 --- /dev/null +++ b/src/node/models/MapType.ts @@ -0,0 +1,3 @@ +export type MapType = { + [key: string|number]: string|number +} \ No newline at end of file diff --git a/src/node/models/PadType.ts b/src/node/models/PadType.ts new file mode 100644 index 000000000..eab10d905 --- /dev/null +++ b/src/node/models/PadType.ts @@ -0,0 +1,16 @@ +export type PadType = { + apool: ()=>APool, + atext: AText, + getInternalRevisionAText: (text:string)=>Promise +} + + +type APool = { + putAttrib: ([],flag: boolean)=>number +} + + +export type AText = { + text: string, + attribs: any +} \ No newline at end of file diff --git a/src/node/models/PromiseWithStd.ts b/src/node/models/PromiseWithStd.ts index 46b03076a..426fcbe54 100644 --- a/src/node/models/PromiseWithStd.ts +++ b/src/node/models/PromiseWithStd.ts @@ -1,5 +1,5 @@ -import {Readable} from "node:stream"; -import {ChildProcess} from "node:child_process"; +import type {Readable} from "node:stream"; +import type {ChildProcess} from "node:child_process"; export type PromiseWithStd = { stdout?: Readable|null, diff --git a/src/node/security/SecretRotator.js b/src/node/security/SecretRotator.ts similarity index 88% rename from src/node/security/SecretRotator.js rename to src/node/security/SecretRotator.ts index 3cc08a01c..780810e91 100644 --- a/src/node/security/SecretRotator.js +++ b/src/node/security/SecretRotator.ts @@ -1,27 +1,32 @@ 'use strict'; +import {DeriveModel} from "../models/DeriveModel"; +import {LegacyParams} from "../models/LegacyParams"; + const {Buffer} = require('buffer'); const crypto = require('./crypto'); const db = require('../db/DB'); const log4js = require('log4js'); class Kdf { - async generateParams() { throw new Error('not implemented'); } - async derive(params, info) { throw new Error('not implemented'); } + async generateParams(): Promise<{ salt: string; digest: string; keyLen: number; secret: string }> { throw new Error('not implemented'); } + async derive(params: DeriveModel, info: any) { throw new Error('not implemented'); } } class LegacyStaticSecret extends Kdf { - async derive(params, info) { return params; } + async derive(params:any, info:any) { return params; } } class Hkdf extends Kdf { - constructor(digest, keyLen) { + private readonly _digest: string + private readonly _keyLen: number + constructor(digest:string, keyLen:number) { super(); this._digest = digest; this._keyLen = keyLen; } - async generateParams() { + async generateParams(): Promise<{ salt: string; digest: string; keyLen: number; secret: string }> { const [secret, salt] = (await Promise.all([ crypto.randomBytes(this._keyLen), crypto.randomBytes(this._keyLen), @@ -29,7 +34,7 @@ class Hkdf extends Kdf { return {digest: this._digest, keyLen: this._keyLen, salt, secret}; } - async derive(p, info) { + async derive(p: DeriveModel, info:any) { return Buffer.from( await crypto.hkdf(p.digest, p.secret, p.salt, info, p.keyLen)).toString('hex'); } @@ -48,8 +53,8 @@ const algorithms = [ const defaultAlgId = algorithms.length - 1; // In JavaScript, the % operator is remainder, not modulus. -const mod = (a, n) => ((a % n) + n) % n; -const intervalStart = (t, interval) => t - mod(t, interval); +const mod = (a:number, n:number) => ((a % n) + n) % n; +const intervalStart = (t:number, interval:number) => t - mod(t, interval); /** * Maintains an array of secrets across one or more Etherpad instances sharing the same database, @@ -59,6 +64,14 @@ const intervalStart = (t, interval) => t - mod(t, interval); * from a long-lived secret stored in the database (generated if missing). */ class SecretRotator { + private readonly secrets: string[]; + private readonly _dbPrefix + private readonly _interval + private readonly _legacyStaticSecret + private readonly _lifetime + private readonly _logger + private _updateTimeout:any + private readonly _t /** * @param {string} dbPrefix - Database key prefix to use for tracking secret metadata. * @param {number} interval - How often to rotate in a new secret. @@ -68,7 +81,7 @@ class SecretRotator { * rotation. If the oldest known secret starts after `lifetime` ago, this secret will cover * the time period starting `lifetime` ago and ending at the start of that secret. */ - constructor(dbPrefix, interval, lifetime, legacyStaticSecret = null) { + constructor(dbPrefix: string, interval: number, lifetime: number, legacyStaticSecret:string|null = null) { /** * The secrets. The first secret in this array is the one that should be used to generate new * MACs. All of the secrets in this array should be used when attempting to authenticate an @@ -94,7 +107,7 @@ class SecretRotator { this._t = {now: Date.now.bind(Date), setTimeout, clearTimeout, algorithms}; } - async _publish(params, id = null) { + async _publish(params: LegacyParams, id:string|null = null) { // Params are published to the db with a randomly generated key to avoid race conditions with // other instances. if (id == null) id = `${this._dbPrefix}:${(await crypto.randomBytes(32)).toString('hex')}`; @@ -114,7 +127,7 @@ class SecretRotator { this._updateTimeout = null; } - async _deriveSecrets(p, now) { + async _deriveSecrets(p: any, now: number) { this._logger.debug('deriving secrets from', p); if (!p.interval) return [await algorithms[p.algId].derive(p.algParams, null)]; const t0 = intervalStart(now, p.interval); @@ -139,7 +152,7 @@ class SecretRotator { // Whether the derived secret for the interval starting at tN is still relevant. If there was no // clock skew, a derived secret is relevant until p.lifetime has elapsed since the end of the // interval. To accommodate clock skew, this end time is extended by p.interval. - const expired = (tN) => now >= tN + (2 * p.interval) + p.lifetime; + const expired = (tN:number) => now >= tN + (2 * p.interval) + p.lifetime; // Walk from t0 back until either the start of coverage or the derived secret is expired. t0 // must always be the first entry in case p is the current params. (The first derived secret is // used for generating MACs, so the secret derived for t0 must be before the secrets derived for @@ -160,12 +173,12 @@ class SecretRotator { // TODO: This is racy. If two instances start up at the same time and there are no existing // matching publications, each will generate and publish their own paramters. In practice this // is unlikely to happen, and if it does it can be fixed by restarting both Etherpad instances. - const dbKeys = await db.findKeys(`${this._dbPrefix}:*`, null) || []; - let currentParams = null; + const dbKeys:string[] = await db.findKeys(`${this._dbPrefix}:*`, null) || []; + let currentParams:any = null; let currentId = null; - const dbWrites = []; + const dbWrites:any[] = []; const allParams = []; - const legacyParams = []; + const legacyParams:LegacyParams[] = []; await Promise.all(dbKeys.map(async (dbKey) => { const p = await db.get(dbKey); if (p.algId === 0 && p.algParams === this._legacyStaticSecret) legacyParams.push(p); @@ -198,7 +211,7 @@ class SecretRotator { !legacyParams.find((p) => p.end + p.lifetime >= legacyEnd + this._lifetime)) { const d = new Date(legacyEnd).toJSON(); this._logger.debug(`adding legacy static secret for ${d} with lifetime ${this._lifetime}`); - const p = { + const p: LegacyParams = { algId: 0, algParams: this._legacyStaticSecret, // The start time is equal to the end time so that this legacy secret does not affect the diff --git a/src/node/security/crypto.js b/src/node/security/crypto.ts similarity index 100% rename from src/node/security/crypto.js rename to src/node/security/crypto.ts diff --git a/src/node/utils/ExportEtherpad.js b/src/node/utils/ExportEtherpad.ts similarity index 96% rename from src/node/utils/ExportEtherpad.js rename to src/node/utils/ExportEtherpad.ts index e20739ad3..292fbcec4 100644 --- a/src/node/utils/ExportEtherpad.js +++ b/src/node/utils/ExportEtherpad.ts @@ -21,13 +21,13 @@ const authorManager = require('../db/AuthorManager'); const hooks = require('../../static/js/pluginfw/hooks'); const padManager = require('../db/PadManager'); -exports.getPadRaw = async (padId, readOnlyId) => { +exports.getPadRaw = async (padId:string, readOnlyId:string) => { const dstPfx = `pad:${readOnlyId || padId}`; const [pad, customPrefixes] = await Promise.all([ padManager.getPad(padId), hooks.aCallAll('exportEtherpadAdditionalContent'), ]); - const pluginRecords = await Promise.all(customPrefixes.map(async (customPrefix) => { + const pluginRecords = await Promise.all(customPrefixes.map(async (customPrefix:string) => { const srcPfx = `${customPrefix}:${padId}`; const dstPfx = `${customPrefix}:${readOnlyId || padId}`; assert(!srcPfx.includes('*')); diff --git a/src/node/utils/ExportHelper.js b/src/node/utils/ExportHelper.ts similarity index 86% rename from src/node/utils/ExportHelper.js rename to src/node/utils/ExportHelper.ts index 48054e7f4..f3a438e86 100644 --- a/src/node/utils/ExportHelper.js +++ b/src/node/utils/ExportHelper.ts @@ -26,7 +26,7 @@ const { checkValidRev } = require('./checkValidRev'); /* * This method seems unused in core and no plugins depend on it */ -exports.getPadPlainText = (pad, revNum) => { +exports.getPadPlainText = (pad: { getInternalRevisionAText: (arg0: any) => any; atext: any; pool: any; }, revNum: undefined) => { const _analyzeLine = exports._analyzeLine; const atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(checkValidRev(revNum)) : pad.atext); const textLines = atext.text.slice(0, -1).split('\n'); @@ -47,10 +47,12 @@ exports.getPadPlainText = (pad, revNum) => { return pieces.join(''); }; +type LineModel = { + [id:string]:string|number|LineModel +} - -exports._analyzeLine = (text, aline, apool) => { - const line = {}; +exports._analyzeLine = (text:string, aline: LineModel, apool: Function) => { + const line: LineModel = {}; // identify list let lineMarker = 0; @@ -86,4 +88,4 @@ exports._analyzeLine = (text, aline, apool) => { exports._encodeWhitespace = - (s) => s.replace(/[^\x21-\x7E\s\t\n\r]/gu, (c) => `&#${c.codePointAt(0)};`); + (s:string) => s.replace(/[^\x21-\x7E\s\t\n\r]/gu, (c) => `&#${c.codePointAt(0)};`); diff --git a/src/node/utils/ExportTxt.js b/src/node/utils/ExportTxt.ts similarity index 92% rename from src/node/utils/ExportTxt.js rename to src/node/utils/ExportTxt.ts index 9511dd0e7..0f793047d 100644 --- a/src/node/utils/ExportTxt.js +++ b/src/node/utils/ExportTxt.ts @@ -19,13 +19,16 @@ * limitations under the License. */ +import {AText, PadType} from "../models/PadType"; +import {MapType} from "../models/MapType"; + const Changeset = require('../../static/js/Changeset'); const attributes = require('../../static/js/attributes'); const padManager = require('../db/PadManager'); const _analyzeLine = require('./ExportHelper')._analyzeLine; // This is slightly different than the HTML method as it passes the output to getTXTFromAText -const getPadTXT = async (pad, revNum) => { +const getPadTXT = async (pad: PadType, revNum: string) => { let atext = pad.atext; if (revNum !== undefined) { @@ -39,13 +42,13 @@ const getPadTXT = async (pad, revNum) => { // This is different than the functionality provided in ExportHtml as it provides formatting // functionality that is designed specifically for TXT exports -const getTXTFromAtext = (pad, atext, authorColors) => { +const getTXTFromAtext = (pad: PadType, atext: AText, authorColors?:string) => { const apool = pad.apool(); const textLines = atext.text.slice(0, -1).split('\n'); const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); const props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough']; - const anumMap = {}; + const anumMap: MapType = {}; const css = ''; props.forEach((propName, i) => { @@ -55,8 +58,8 @@ const getTXTFromAtext = (pad, atext, authorColors) => { } }); - const getLineTXT = (text, attribs) => { - const propVals = [false, false, false]; + const getLineTXT = (text:string, attribs:any) => { + const propVals:(number|boolean)[] = [false, false, false]; const ENTER = 1; const STAY = 2; const LEAVE = 0; @@ -71,7 +74,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => { let idx = 0; - const processNextChars = (numChars) => { + const processNextChars = (numChars: number) => { if (numChars <= 0) { return; } @@ -84,7 +87,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => { for (const a of attributes.decodeAttribString(o.attribs)) { if (a in anumMap) { - const i = anumMap[a]; // i = 0 => bold, etc. + const i = anumMap[a] as number; // i = 0 => bold, etc. if (!propVals[i]) { propVals[i] = ENTER; @@ -189,7 +192,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => { // want to deal gracefully with blank lines. // => keeps track of the parents level of indentation - const listNumbers = {}; + const listNumbers:MapType = {}; let prevListLevel; for (let i = 0; i < textLines.length; i++) { @@ -233,6 +236,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => { delete listNumbers[prevListLevel]; } + // @ts-ignore listNumbers[line.listLevel]++; if (line.listLevel > 1) { let x = 1; @@ -258,7 +262,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => { exports.getTXTFromAtext = getTXTFromAtext; -exports.getPadTXTDocument = async (padId, revNum) => { +exports.getPadTXTDocument = async (padId:string, revNum:string) => { const pad = await padManager.getPad(padId); return getPadTXT(pad, revNum); }; diff --git a/src/node/utils/checkValidRev.js b/src/node/utils/checkValidRev.ts similarity index 81% rename from src/node/utils/checkValidRev.js rename to src/node/utils/checkValidRev.ts index 862c6a2bd..5367ddf99 100644 --- a/src/node/utils/checkValidRev.js +++ b/src/node/utils/checkValidRev.ts @@ -4,7 +4,7 @@ const CustomError = require('../utils/customError'); // checks if a rev is a legal number // pre-condition is that `rev` is not undefined -const checkValidRev = (rev) => { +const checkValidRev = (rev: number|string) => { if (typeof rev !== 'number') { rev = parseInt(rev, 10); } @@ -28,7 +28,7 @@ const checkValidRev = (rev) => { }; // checks if a number is an int -const isInt = (value) => (parseFloat(value) === parseInt(value, 10)) && !isNaN(value); +const isInt = (value:number) => (parseFloat(String(value)) === parseInt(String(value), 10)) && !isNaN(value); exports.isInt = isInt; exports.checkValidRev = checkValidRev; diff --git a/src/node/utils/customError.js b/src/node/utils/customError.ts similarity index 85% rename from src/node/utils/customError.js rename to src/node/utils/customError.ts index 24ad181e6..c58360269 100644 --- a/src/node/utils/customError.js +++ b/src/node/utils/customError.ts @@ -10,11 +10,11 @@ class CustomError extends Error { /** * Creates an instance of CustomError. - * @param {*} message + * @param {string} message * @param {string} [name='Error'] a custom name for the error object * @memberof CustomError */ - constructor(message, name = 'Error') { + constructor(message:string, name: string = 'Error') { super(message); this.name = name; Error.captureStackTrace(this, this.constructor); diff --git a/src/node/utils/path_exists.js b/src/node/utils/path_exists.ts similarity index 65% rename from src/node/utils/path_exists.js rename to src/node/utils/path_exists.ts index 0b4c8fe94..354cd3cc7 100644 --- a/src/node/utils/path_exists.js +++ b/src/node/utils/path_exists.ts @@ -1,8 +1,8 @@ 'use strict'; const fs = require('fs'); -const check = (path) => { - const existsSync = fs.statSync || fs.existsSync || path.existsSync; +const check = (path:string) => { + const existsSync = fs.statSync || fs.existsSync; let result; try {