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 {ChildProcess} from "node:child_process";
import type {Readable} from "node:stream";
import type {ChildProcess} from "node:child_process";
export type PromiseWithStd = {
stdout?: Readable|null,

View file

@ -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

View file

@ -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('*'));

View file

@ -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)};`);

View file

@ -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);
};

View file

@ -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;

View file

@ -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);

View file

@ -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 {