Moved SecretRotator to typescript.

This commit is contained in:
SamTV12345 2024-01-24 09:44:17 +01:00
parent d5c011e6ed
commit a57b13d319
13 changed files with 93 additions and 41 deletions

View file

@ -0,0 +1,6 @@
export type DeriveModel = {
digest: string,
secret: string,
salt: string,
keyLen: number
}

View file

@ -0,0 +1,8 @@
export type LegacyParams = {
start: number,
end: number,
lifetime: number,
algId: number,
algParams: any,
interval:number|null
}

View file

@ -0,0 +1,3 @@
export type MapType = {
[key: string|number]: string|number
}

View file

@ -0,0 +1,16 @@
export type PadType = {
apool: ()=>APool,
atext: AText,
getInternalRevisionAText: (text:string)=>Promise<AText>
}
type APool = {
putAttrib: ([],flag: boolean)=>number
}
export type AText = {
text: string,
attribs: any
}

View file

@ -1,5 +1,5 @@
import {Readable} from "node:stream"; import type {Readable} from "node:stream";
import {ChildProcess} from "node:child_process"; import type {ChildProcess} from "node:child_process";
export type PromiseWithStd = { export type PromiseWithStd = {
stdout?: Readable|null, stdout?: Readable|null,

View file

@ -1,27 +1,32 @@
'use strict'; 'use strict';
import {DeriveModel} from "../models/DeriveModel";
import {LegacyParams} from "../models/LegacyParams";
const {Buffer} = require('buffer'); const {Buffer} = require('buffer');
const crypto = require('./crypto'); const crypto = require('./crypto');
const db = require('../db/DB'); const db = require('../db/DB');
const log4js = require('log4js'); const log4js = require('log4js');
class Kdf { class Kdf {
async generateParams() { throw new Error('not implemented'); } async generateParams(): Promise<{ salt: string; digest: string; keyLen: number; secret: string }> { throw new Error('not implemented'); }
async derive(params, info) { throw new Error('not implemented'); } async derive(params: DeriveModel, info: any) { throw new Error('not implemented'); }
} }
class LegacyStaticSecret extends Kdf { class LegacyStaticSecret extends Kdf {
async derive(params, info) { return params; } async derive(params:any, info:any) { return params; }
} }
class Hkdf extends Kdf { class Hkdf extends Kdf {
constructor(digest, keyLen) { private readonly _digest: string
private readonly _keyLen: number
constructor(digest:string, keyLen:number) {
super(); super();
this._digest = digest; this._digest = digest;
this._keyLen = keyLen; this._keyLen = keyLen;
} }
async generateParams() { async generateParams(): Promise<{ salt: string; digest: string; keyLen: number; secret: string }> {
const [secret, salt] = (await Promise.all([ const [secret, salt] = (await Promise.all([
crypto.randomBytes(this._keyLen), crypto.randomBytes(this._keyLen),
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}; return {digest: this._digest, keyLen: this._keyLen, salt, secret};
} }
async derive(p, info) { async derive(p: DeriveModel, info:any) {
return Buffer.from( return Buffer.from(
await crypto.hkdf(p.digest, p.secret, p.salt, info, p.keyLen)).toString('hex'); 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; const defaultAlgId = algorithms.length - 1;
// In JavaScript, the % operator is remainder, not modulus. // In JavaScript, the % operator is remainder, not modulus.
const mod = (a, n) => ((a % n) + n) % n; const mod = (a:number, n:number) => ((a % n) + n) % n;
const intervalStart = (t, interval) => t - mod(t, interval); 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, * 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). * from a long-lived secret stored in the database (generated if missing).
*/ */
class SecretRotator { 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 {string} dbPrefix - Database key prefix to use for tracking secret metadata.
* @param {number} interval - How often to rotate in a new secret. * @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 * 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. * 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 * 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 * 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}; 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 // Params are published to the db with a randomly generated key to avoid race conditions with
// other instances. // other instances.
if (id == null) id = `${this._dbPrefix}:${(await crypto.randomBytes(32)).toString('hex')}`; if (id == null) id = `${this._dbPrefix}:${(await crypto.randomBytes(32)).toString('hex')}`;
@ -114,7 +127,7 @@ class SecretRotator {
this._updateTimeout = null; this._updateTimeout = null;
} }
async _deriveSecrets(p, now) { async _deriveSecrets(p: any, now: number) {
this._logger.debug('deriving secrets from', p); this._logger.debug('deriving secrets from', p);
if (!p.interval) return [await algorithms[p.algId].derive(p.algParams, null)]; if (!p.interval) return [await algorithms[p.algId].derive(p.algParams, null)];
const t0 = intervalStart(now, p.interval); 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 // 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 // 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. // 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 // 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 // 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 // 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 // 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 // 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. // 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) || []; const dbKeys:string[] = await db.findKeys(`${this._dbPrefix}:*`, null) || [];
let currentParams = null; let currentParams:any = null;
let currentId = null; let currentId = null;
const dbWrites = []; const dbWrites:any[] = [];
const allParams = []; const allParams = [];
const legacyParams = []; const legacyParams:LegacyParams[] = [];
await Promise.all(dbKeys.map(async (dbKey) => { await Promise.all(dbKeys.map(async (dbKey) => {
const p = await db.get(dbKey); const p = await db.get(dbKey);
if (p.algId === 0 && p.algParams === this._legacyStaticSecret) legacyParams.push(p); 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)) { !legacyParams.find((p) => p.end + p.lifetime >= legacyEnd + this._lifetime)) {
const d = new Date(legacyEnd).toJSON(); const d = new Date(legacyEnd).toJSON();
this._logger.debug(`adding legacy static secret for ${d} with lifetime ${this._lifetime}`); this._logger.debug(`adding legacy static secret for ${d} with lifetime ${this._lifetime}`);
const p = { const p: LegacyParams = {
algId: 0, algId: 0,
algParams: this._legacyStaticSecret, algParams: this._legacyStaticSecret,
// The start time is equal to the end time so that this legacy secret does not affect the // The start time is equal to the end time so that this legacy secret does not affect the

View file

@ -21,13 +21,13 @@ const authorManager = require('../db/AuthorManager');
const hooks = require('../../static/js/pluginfw/hooks'); const hooks = require('../../static/js/pluginfw/hooks');
const padManager = require('../db/PadManager'); const padManager = require('../db/PadManager');
exports.getPadRaw = async (padId, readOnlyId) => { exports.getPadRaw = async (padId:string, readOnlyId:string) => {
const dstPfx = `pad:${readOnlyId || padId}`; const dstPfx = `pad:${readOnlyId || padId}`;
const [pad, customPrefixes] = await Promise.all([ const [pad, customPrefixes] = await Promise.all([
padManager.getPad(padId), padManager.getPad(padId),
hooks.aCallAll('exportEtherpadAdditionalContent'), 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 srcPfx = `${customPrefix}:${padId}`;
const dstPfx = `${customPrefix}:${readOnlyId || padId}`; const dstPfx = `${customPrefix}:${readOnlyId || padId}`;
assert(!srcPfx.includes('*')); assert(!srcPfx.includes('*'));

View file

@ -26,7 +26,7 @@ const { checkValidRev } = require('./checkValidRev');
/* /*
* This method seems unused in core and no plugins depend on it * 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 _analyzeLine = exports._analyzeLine;
const atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(checkValidRev(revNum)) : pad.atext); const atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(checkValidRev(revNum)) : pad.atext);
const textLines = atext.text.slice(0, -1).split('\n'); const textLines = atext.text.slice(0, -1).split('\n');
@ -47,10 +47,12 @@ exports.getPadPlainText = (pad, revNum) => {
return pieces.join(''); return pieces.join('');
}; };
type LineModel = {
[id:string]:string|number|LineModel
}
exports._analyzeLine = (text:string, aline: LineModel, apool: Function) => {
exports._analyzeLine = (text, aline, apool) => { const line: LineModel = {};
const line = {};
// identify list // identify list
let lineMarker = 0; let lineMarker = 0;
@ -86,4 +88,4 @@ exports._analyzeLine = (text, aline, apool) => {
exports._encodeWhitespace = 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)};`);

View file

@ -19,13 +19,16 @@
* limitations under the License. * limitations under the License.
*/ */
import {AText, PadType} from "../models/PadType";
import {MapType} from "../models/MapType";
const Changeset = require('../../static/js/Changeset'); const Changeset = require('../../static/js/Changeset');
const attributes = require('../../static/js/attributes'); const attributes = require('../../static/js/attributes');
const padManager = require('../db/PadManager'); const padManager = require('../db/PadManager');
const _analyzeLine = require('./ExportHelper')._analyzeLine; const _analyzeLine = require('./ExportHelper')._analyzeLine;
// This is slightly different than the HTML method as it passes the output to getTXTFromAText // 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; let atext = pad.atext;
if (revNum !== undefined) { 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 // This is different than the functionality provided in ExportHtml as it provides formatting
// functionality that is designed specifically for TXT exports // 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 apool = pad.apool();
const textLines = atext.text.slice(0, -1).split('\n'); const textLines = atext.text.slice(0, -1).split('\n');
const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
const props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough']; const props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough'];
const anumMap = {}; const anumMap: MapType = {};
const css = ''; const css = '';
props.forEach((propName, i) => { props.forEach((propName, i) => {
@ -55,8 +58,8 @@ const getTXTFromAtext = (pad, atext, authorColors) => {
} }
}); });
const getLineTXT = (text, attribs) => { const getLineTXT = (text:string, attribs:any) => {
const propVals = [false, false, false]; const propVals:(number|boolean)[] = [false, false, false];
const ENTER = 1; const ENTER = 1;
const STAY = 2; const STAY = 2;
const LEAVE = 0; const LEAVE = 0;
@ -71,7 +74,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => {
let idx = 0; let idx = 0;
const processNextChars = (numChars) => { const processNextChars = (numChars: number) => {
if (numChars <= 0) { if (numChars <= 0) {
return; return;
} }
@ -84,7 +87,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => {
for (const a of attributes.decodeAttribString(o.attribs)) { for (const a of attributes.decodeAttribString(o.attribs)) {
if (a in anumMap) { 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]) { if (!propVals[i]) {
propVals[i] = ENTER; propVals[i] = ENTER;
@ -189,7 +192,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => {
// want to deal gracefully with blank lines. // want to deal gracefully with blank lines.
// => keeps track of the parents level of indentation // => keeps track of the parents level of indentation
const listNumbers = {}; const listNumbers:MapType = {};
let prevListLevel; let prevListLevel;
for (let i = 0; i < textLines.length; i++) { for (let i = 0; i < textLines.length; i++) {
@ -233,6 +236,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => {
delete listNumbers[prevListLevel]; delete listNumbers[prevListLevel];
} }
// @ts-ignore
listNumbers[line.listLevel]++; listNumbers[line.listLevel]++;
if (line.listLevel > 1) { if (line.listLevel > 1) {
let x = 1; let x = 1;
@ -258,7 +262,7 @@ const getTXTFromAtext = (pad, atext, authorColors) => {
exports.getTXTFromAtext = getTXTFromAtext; exports.getTXTFromAtext = getTXTFromAtext;
exports.getPadTXTDocument = async (padId, revNum) => { exports.getPadTXTDocument = async (padId:string, revNum:string) => {
const pad = await padManager.getPad(padId); const pad = await padManager.getPad(padId);
return getPadTXT(pad, revNum); return getPadTXT(pad, revNum);
}; };

View file

@ -4,7 +4,7 @@ const CustomError = require('../utils/customError');
// checks if a rev is a legal number // checks if a rev is a legal number
// pre-condition is that `rev` is not undefined // pre-condition is that `rev` is not undefined
const checkValidRev = (rev) => { const checkValidRev = (rev: number|string) => {
if (typeof rev !== 'number') { if (typeof rev !== 'number') {
rev = parseInt(rev, 10); rev = parseInt(rev, 10);
} }
@ -28,7 +28,7 @@ const checkValidRev = (rev) => {
}; };
// checks if a number is an int // 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.isInt = isInt;
exports.checkValidRev = checkValidRev; exports.checkValidRev = checkValidRev;

View file

@ -10,11 +10,11 @@
class CustomError extends Error { class CustomError extends Error {
/** /**
* Creates an instance of CustomError. * Creates an instance of CustomError.
* @param {*} message * @param {string} message
* @param {string} [name='Error'] a custom name for the error object * @param {string} [name='Error'] a custom name for the error object
* @memberof CustomError * @memberof CustomError
*/ */
constructor(message, name = 'Error') { constructor(message:string, name: string = 'Error') {
super(message); super(message);
this.name = name; this.name = name;
Error.captureStackTrace(this, this.constructor); Error.captureStackTrace(this, this.constructor);

View file

@ -1,8 +1,8 @@
'use strict'; 'use strict';
const fs = require('fs'); const fs = require('fs');
const check = (path) => { const check = (path:string) => {
const existsSync = fs.statSync || fs.existsSync || path.existsSync; const existsSync = fs.statSync || fs.existsSync;
let result; let result;
try { try {