/*! * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import type {Crypto, Hash} from "./Messaging"; import {AssocArrayCollector} from "./SourcesCollectors"; import {LazyStream} from "./Stream"; /* * Some crypto implementations which might come in handy */ /** * basic json stringify encryption impl * this does not really full encryption except for a standard json stringyfywith an encapsulation json * * the return value resembles: *
 *     {
 *         encryptedData: 
 *     }
 * 
*/ export class JSONCrypto implements Crypto { decode(data: any): any { if (data?.encryptedData) { return JSON.parse(data.encryptedData); } return data; } encode(data: any) { return { encryptedData: JSON.stringify(data) } } } /** * a class with timeout functionality which blocks decodes after a certain period of time * if the message is not decoded by then * We use hash as identifier generation after encryption to make sure * a trace was possible * * The idea behind this is to have a generic wrapper which allows messages with dynamic encryption * where keys/salts only exist for a certain period of time before expiring! * That way someone who implements such a scheme does not have to take care about the bookkeeping mechanisms! * Or you can use crypto mechanisms which do not have expiring keys and still expire them automatically * * I will leave it up to the system integrator to provide a rotating crypto class, because this is highly * implementation dependent. But it helps to have a wrapper! */ export class ExpiringCrypto implements Crypto { private static MAX_GC_CYCLES = 10; private gcCycleCnt = 0; private storedMessages: { [key: string]: number } = {}; private lastCall = 0; /** * @param timeout timeout in milliseconds until a message is expired * @param parentCrypto the embedded decorated crypto algorithm * @param hashSum hashshum implementation to generate a hash */ constructor(private timeout: number, private parentCrypto: Crypto, private hashSum: Hash) { } /** * decode implementation with a timeout hook install * @param data */ decode(data: any): any { //if ((this.gcCycleCnt++ % ExpiringCrypto.MAX_GC_CYCLES) === 0) { const currTime = new Date().getTime(); if(this.gcLimitReached(currTime)) { this.storedMessages = LazyStream .ofAssoc(this.storedMessages) .filter(data => data[1] >= currTime) .collect(new AssocArrayCollector()); } this.lastCall = currTime; let rotatingEncoded = this.hashSum.encode(data); if (!this.storedMessages?.[rotatingEncoded.toString()]) { throw Error("An item was tried to be decrypted which either was expired or invalid"); } return this.parentCrypto.decode(data); } /** * trigger function to determine whether the gc needs to cycle again, this is either time or call based * the gc itself collects only on expiration dates * The idea is to run this operation only occasionally because it is costly * We also could have used timeouts etc.. but those would need shutdown/destroy cleanups * * @param currTime * @private */ private gcLimitReached(currTime: number) { return (this.lastCall + this.timeout) < currTime || ((++this.gcCycleCnt) % ExpiringCrypto.MAX_GC_CYCLES == 0); } /** * encode with a timeout hook installed * calls the encode of the delegated object * * @param data */ encode(data: any): any { let encoded = this.parentCrypto.encode(data); //ok use the hashsum really only to store expirations, theoretically there could be a second message which does not invalidate the first one //but this is very unlikely unless a message is sent over and over again, in this case we have a timeout extension anyway! let rotatingEncoded = this.hashSum.encode(encoded); this.storedMessages[rotatingEncoded.toString()] = (new Date().getTime()) + this.timeout; return encoded; } }