mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-05-08 08:01:02 -04:00
Moved SecretRotator to typescript.
This commit is contained in:
parent
d5c011e6ed
commit
a57b13d319
13 changed files with 93 additions and 41 deletions
6
src/node/models/DeriveModel.ts
Normal file
6
src/node/models/DeriveModel.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export type DeriveModel = {
|
||||
digest: string,
|
||||
secret: string,
|
||||
salt: string,
|
||||
keyLen: number
|
||||
}
|
8
src/node/models/LegacyParams.ts
Normal file
8
src/node/models/LegacyParams.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
export type LegacyParams = {
|
||||
start: number,
|
||||
end: number,
|
||||
lifetime: number,
|
||||
algId: number,
|
||||
algParams: any,
|
||||
interval:number|null
|
||||
}
|
3
src/node/models/MapType.ts
Normal file
3
src/node/models/MapType.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export type MapType = {
|
||||
[key: string|number]: string|number
|
||||
}
|
16
src/node/models/PadType.ts
Normal file
16
src/node/models/PadType.ts
Normal 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
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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('*'));
|
|
@ -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)};`);
|
|
@ -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);
|
||||
};
|
|
@ -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;
|
|
@ -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);
|
|
@ -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 {
|
Loading…
Add table
Add a link
Reference in a new issue